@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,296 @@
1
+ # Expression
2
+
3
+ Dialect-independent SQL expression builder. Generates JSON AST (`Expr`) instead of SQL strings, which `QueryBuilder` converts to each DBMS dialect.
4
+
5
+ Source: `src/expr/expr-unit.ts`, `src/expr/expr.ts`
6
+
7
+ ## ExprUnit
8
+
9
+ Type-safe expression wrapper. Tracks expression return type using TypeScript generics.
10
+
11
+ ```typescript
12
+ class ExprUnit<TPrimitive extends ColumnPrimitive> {
13
+ readonly $infer!: TPrimitive;
14
+ readonly dataType: ColumnPrimitiveStr;
15
+ readonly expr: Expr;
16
+
17
+ /** Strip undefined from the type (non-null assertion) */
18
+ get n(): ExprUnit<NonNullable<TPrimitive>>;
19
+
20
+ constructor(dataType: ColumnPrimitiveStr, expr: Expr);
21
+ }
22
+ ```
23
+
24
+ ## WhereExprUnit
25
+
26
+ Expression wrapper for WHERE clause conditions.
27
+
28
+ ```typescript
29
+ class WhereExprUnit {
30
+ readonly expr: WhereExpr;
31
+ constructor(expr: WhereExpr);
32
+ }
33
+ ```
34
+
35
+ ## ExprInput
36
+
37
+ Input type that accepts either an `ExprUnit` or a literal value.
38
+
39
+ ```typescript
40
+ type ExprInput<TPrimitive extends ColumnPrimitive> = ExprUnit<TPrimitive> | TPrimitive;
41
+ ```
42
+
43
+ ## SwitchExprBuilder
44
+
45
+ Builder interface returned by `expr.switch()` for CASE WHEN expressions.
46
+
47
+ ```typescript
48
+ interface SwitchExprBuilder<TPrimitive extends ColumnPrimitive> {
49
+ case(condition: WhereExprUnit, then: ExprInput<TPrimitive>): SwitchExprBuilder<TPrimitive>;
50
+ default(value: ExprInput<TPrimitive>): ExprUnit<TPrimitive>;
51
+ }
52
+ ```
53
+
54
+ ---
55
+
56
+ ## expr
57
+
58
+ The main expression builder object. All methods return `ExprUnit` or `WhereExprUnit`.
59
+
60
+ ### Value Creation
61
+
62
+ | Method | Signature | Description |
63
+ |--------|-----------|-------------|
64
+ | `val` | `val<TStr>(dataType: TStr, value: T): ExprUnit` | Wrap literal value as expression |
65
+ | `col` | `col<TStr>(dataType: ColumnPrimitiveStr, ...path: string[]): ExprUnit` | Column reference (internal use) |
66
+ | `raw` | `raw<T>(dataType: T): (strings, ...values) => ExprUnit` | Raw SQL tagged template (escape hatch) |
67
+
68
+ ```typescript
69
+ expr.val("string", "active")
70
+ expr.val("number", 100)
71
+ expr.val("DateTime", DateTime.now())
72
+
73
+ // Raw SQL
74
+ expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`
75
+ ```
76
+
77
+ ### Comparison Operators (WHERE)
78
+
79
+ | Method | SQL | Description |
80
+ |--------|-----|-------------|
81
+ | `eq(source, target)` | `<=>` / `IS NULL OR =` | Equality (NULL-safe) |
82
+ | `gt(source, target)` | `>` | Greater than |
83
+ | `lt(source, target)` | `<` | Less than |
84
+ | `gte(source, target)` | `>=` | Greater than or equal |
85
+ | `lte(source, target)` | `<=` | Less than or equal |
86
+ | `between(source, from?, to?)` | `BETWEEN` | Range (undefined = unbounded) |
87
+
88
+ ```typescript
89
+ db.user().where((u) => [
90
+ expr.eq(u.status, "active"),
91
+ expr.gte(u.age, 18),
92
+ expr.between(u.score, 60, 100),
93
+ ])
94
+ ```
95
+
96
+ ### NULL Check
97
+
98
+ | Method | SQL | Description |
99
+ |--------|-----|-------------|
100
+ | `null(source)` | `IS NULL` | Check if value is NULL |
101
+
102
+ ### String Search (WHERE)
103
+
104
+ | Method | SQL | Description |
105
+ |--------|-----|-------------|
106
+ | `like(source, pattern)` | `LIKE ... ESCAPE '\'` | Pattern matching (`%`, `_` wildcards) |
107
+ | `regexp(source, pattern)` | `REGEXP` | Regular expression matching |
108
+
109
+ ### IN / EXISTS (WHERE)
110
+
111
+ | Method | SQL | Description |
112
+ |--------|-----|-------------|
113
+ | `in(source, values)` | `IN (...)` | Value list inclusion |
114
+ | `inQuery(source, query)` | `IN (SELECT ...)` | Subquery inclusion (single column) |
115
+ | `exists(query)` | `EXISTS (SELECT ...)` | Subquery existence check |
116
+
117
+ ```typescript
118
+ db.user().where((u) => [
119
+ expr.in(u.status, ["active", "pending"]),
120
+ expr.exists(
121
+ db.order().where((o) => [expr.eq(o.userId, u.id)])
122
+ ),
123
+ ])
124
+ ```
125
+
126
+ ### Logical Operators (WHERE)
127
+
128
+ | Method | SQL | Description |
129
+ |--------|-----|-------------|
130
+ | `not(arg)` | `NOT (...)` | Negate a condition |
131
+ | `and(conditions)` | `... AND ...` | All conditions must be true |
132
+ | `or(conditions)` | `... OR ...` | At least one must be true |
133
+
134
+ ### String Functions (SELECT)
135
+
136
+ | Method | SQL | Description |
137
+ |--------|-----|-------------|
138
+ | `concat(...args)` | `CONCAT(...)` | String concatenation (NULL-safe) |
139
+ | `left(source, length)` | `LEFT(...)` | Extract from left |
140
+ | `right(source, length)` | `RIGHT(...)` | Extract from right |
141
+ | `trim(source)` | `TRIM(...)` | Remove whitespace |
142
+ | `padStart(source, length, fill)` | `LPAD(...)` | Left padding |
143
+ | `replace(source, from, to)` | `REPLACE(...)` | String replacement |
144
+ | `upper(source)` | `UPPER(...)` | Uppercase |
145
+ | `lower(source)` | `LOWER(...)` | Lowercase |
146
+ | `length(source)` | `CHAR_LENGTH(...)` | Character count |
147
+ | `byteLength(source)` | `OCTET_LENGTH(...)` | Byte count |
148
+ | `substring(source, start, length?)` | `SUBSTRING(...)` | Extract substring (1-based) |
149
+ | `indexOf(source, search)` | `LOCATE(...)`/`CHARINDEX(...)` | Find position (1-based, 0 if not found) |
150
+
151
+ ```typescript
152
+ db.user().select((u) => ({
153
+ fullName: expr.concat(u.firstName, " ", u.lastName),
154
+ initial: expr.left(u.name, 1),
155
+ email: expr.lower(u.email),
156
+ }))
157
+ ```
158
+
159
+ ### Numeric Functions (SELECT)
160
+
161
+ | Method | SQL | Description |
162
+ |--------|-----|-------------|
163
+ | `abs(source)` | `ABS(...)` | Absolute value |
164
+ | `round(source, digits)` | `ROUND(...)` | Round to N digits |
165
+ | `ceil(source)` | `CEILING(...)` | Ceiling |
166
+ | `floor(source)` | `FLOOR(...)` | Floor |
167
+
168
+ ### Date Functions (SELECT)
169
+
170
+ | Method | SQL | Description |
171
+ |--------|-----|-------------|
172
+ | `year(source)` | `YEAR(...)` | Extract year |
173
+ | `month(source)` | `MONTH(...)` | Extract month (1-12) |
174
+ | `day(source)` | `DAY(...)` | Extract day (1-31) |
175
+ | `hour(source)` | `HOUR(...)` | Extract hour (0-23) |
176
+ | `minute(source)` | `MINUTE(...)` | Extract minute (0-59) |
177
+ | `second(source)` | `SECOND(...)` | Extract second (0-59) |
178
+ | `isoWeek(source)` | `WEEK(..., 3)` | ISO week number (1-53) |
179
+ | `isoWeekStartDate(source)` | (computed) | Monday of the week |
180
+ | `isoYearMonth(source)` | (computed) | First day of the month |
181
+ | `dateDiff(unit, from, to)` | `DATEDIFF(...)` | Date difference |
182
+ | `dateAdd(unit, source, value)` | `DATEADD(...)` | Add to date |
183
+ | `formatDate(source, format)` | `DATE_FORMAT(...)` | Format date as string |
184
+
185
+ `DateUnit` values: `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`
186
+
187
+ ```typescript
188
+ db.user().select((u) => ({
189
+ age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
190
+ expiresAt: expr.dateAdd("month", u.startDate, 12),
191
+ }))
192
+ ```
193
+
194
+ ### Conditional Functions (SELECT)
195
+
196
+ | Method | SQL | Description |
197
+ |--------|-----|-------------|
198
+ | `coalesce(...args)` | `COALESCE(...)` | First non-null value |
199
+ | `nullIf(source, value)` | `NULLIF(...)` | Return NULL if equal |
200
+ | `is(condition)` | (computed) | Transform WHERE to boolean column |
201
+ | `switch<T>()` | `CASE WHEN ... END` | CASE WHEN builder |
202
+ | `if(condition, then, else_)` | `IF(...)`/`IIF(...)` | Ternary conditional |
203
+
204
+ ```typescript
205
+ db.user().select((u) => ({
206
+ displayName: expr.coalesce(u.nickname, u.name, "Guest"),
207
+ isActive: expr.is(expr.eq(u.status, "active")),
208
+ grade: expr.switch<string>()
209
+ .case(expr.gte(u.score, 90), "A")
210
+ .case(expr.gte(u.score, 80), "B")
211
+ .default("C"),
212
+ }))
213
+ ```
214
+
215
+ ### Aggregate Functions (SELECT)
216
+
217
+ | Method | SQL | Description |
218
+ |--------|-----|-------------|
219
+ | `count(arg?, distinct?)` | `COUNT(...)` | Row count |
220
+ | `sum(arg)` | `SUM(...)` | Sum (NULL ignored) |
221
+ | `avg(arg)` | `AVG(...)` | Average (NULL ignored) |
222
+ | `max(arg)` | `MAX(...)` | Maximum value |
223
+ | `min(arg)` | `MIN(...)` | Minimum value |
224
+
225
+ ### Other Functions (SELECT)
226
+
227
+ | Method | SQL | Description |
228
+ |--------|-----|-------------|
229
+ | `greatest(...args)` | `GREATEST(...)` | Greatest among values |
230
+ | `least(...args)` | `LEAST(...)` | Least among values |
231
+ | `rowNum()` | (computed) | Row number (no OVER) |
232
+ | `random()` | `RAND()`/`RANDOM()` | Random number (0-1) |
233
+ | `cast(source, targetType)` | `CAST(... AS ...)` | Type conversion |
234
+ | `subquery(dataType, queryable)` | `(SELECT ...)` | Scalar subquery |
235
+
236
+ ```typescript
237
+ db.user().select((u) => ({
238
+ id: u.id,
239
+ postCount: expr.subquery("number",
240
+ db.post()
241
+ .where((p) => [expr.eq(p.userId, u.id)])
242
+ .select(() => ({ cnt: expr.count() }))
243
+ ),
244
+ }))
245
+ ```
246
+
247
+ ### Window Functions (SELECT)
248
+
249
+ All window functions accept a `WinSpecInput`:
250
+
251
+ ```typescript
252
+ interface WinSpecInput {
253
+ partitionBy?: ExprInput<ColumnPrimitive>[];
254
+ orderBy?: [ExprInput<ColumnPrimitive>, ("ASC" | "DESC")?][];
255
+ }
256
+ ```
257
+
258
+ | Method | SQL | Description |
259
+ |--------|-----|-------------|
260
+ | `rowNumber(spec)` | `ROW_NUMBER() OVER(...)` | Row number within partition |
261
+ | `rank(spec)` | `RANK() OVER(...)` | Rank (ties skip: 1,1,3) |
262
+ | `denseRank(spec)` | `DENSE_RANK() OVER(...)` | Dense rank (ties consecutive: 1,1,2) |
263
+ | `ntile(n, spec)` | `NTILE(n) OVER(...)` | Split into n groups |
264
+ | `lag(column, spec, options?)` | `LAG() OVER(...)` | Previous row value |
265
+ | `lead(column, spec, options?)` | `LEAD() OVER(...)` | Next row value |
266
+ | `firstValue(column, spec)` | `FIRST_VALUE() OVER(...)` | First value in frame |
267
+ | `lastValue(column, spec)` | `LAST_VALUE() OVER(...)` | Last value in frame |
268
+ | `sumOver(column, spec)` | `SUM() OVER(...)` | Window sum |
269
+ | `avgOver(column, spec)` | `AVG() OVER(...)` | Window average |
270
+ | `countOver(spec, column?)` | `COUNT() OVER(...)` | Window count |
271
+ | `minOver(column, spec)` | `MIN() OVER(...)` | Window minimum |
272
+ | `maxOver(column, spec)` | `MAX() OVER(...)` | Window maximum |
273
+
274
+ `lag` and `lead` options:
275
+ - `offset?: number` -- default 1
276
+ - `default?: ExprInput<T>` -- default value when no row exists
277
+
278
+ ```typescript
279
+ db.order().select((o) => ({
280
+ ...o,
281
+ rowNum: expr.rowNumber({
282
+ partitionBy: [o.userId],
283
+ orderBy: [[o.createdAt, "DESC"]],
284
+ }),
285
+ runningTotal: expr.sumOver(o.amount, {
286
+ partitionBy: [o.userId],
287
+ orderBy: [[o.createdAt, "ASC"]],
288
+ }),
289
+ }))
290
+ ```
291
+
292
+ ### Helper
293
+
294
+ | Method | Description |
295
+ |--------|-------------|
296
+ | `toExpr(value: ExprInput<ColumnPrimitive>): Expr` | Convert ExprInput to Expr JSON AST (internal use) |
@@ -0,0 +1,196 @@
1
+ # Query Builder
2
+
3
+ Render `QueryDef` JSON AST to dialect-specific SQL strings.
4
+
5
+ Source: `src/query-builder/query-builder.ts`, `src/query-builder/base/query-builder-base.ts`, `src/query-builder/base/expr-renderer-base.ts`, `src/query-builder/mysql/`, `src/query-builder/mssql/`, `src/query-builder/postgresql/`
6
+
7
+ ## createQueryBuilder
8
+
9
+ Factory function that returns a dialect-specific `QueryBuilderBase` implementation.
10
+
11
+ ```typescript
12
+ function createQueryBuilder(dialect: Dialect): QueryBuilderBase;
13
+ ```
14
+
15
+ | Dialect | QueryBuilder | ExprRenderer |
16
+ |---------|-------------|--------------|
17
+ | `"mysql"` | `MysqlQueryBuilder` | `MysqlExprRenderer` |
18
+ | `"mssql"` | `MssqlQueryBuilder` | `MssqlExprRenderer` |
19
+ | `"postgresql"` | `PostgresqlQueryBuilder` | `PostgresqlExprRenderer` |
20
+
21
+ **Example:**
22
+
23
+ ```typescript
24
+ import { createQueryBuilder } from "@simplysm/orm-common";
25
+
26
+ const builder = createQueryBuilder("mysql");
27
+ const result = builder.build(queryDef);
28
+ console.log(result.sql);
29
+ ```
30
+
31
+ ---
32
+
33
+ ## QueryBuilderBase
34
+
35
+ Abstract base class for `QueryDef` to SQL rendering. Implements common dispatch logic and rendering helpers.
36
+
37
+ ```typescript
38
+ abstract class QueryBuilderBase {
39
+ /**
40
+ * Main entry point: dispatch to the appropriate method based on def.type
41
+ */
42
+ build(def: QueryDef): QueryBuildResult;
43
+ }
44
+ ```
45
+
46
+ ### Design Principles
47
+
48
+ - Method names match `def.type` for dynamic dispatch
49
+ - Only 100% dialect-identical logic is implemented in the base class
50
+ - Any dialect-specific behavior is declared as `abstract`
51
+
52
+ ### Common Render Methods (implemented)
53
+
54
+ | Method | Description |
55
+ |--------|-------------|
56
+ | `renderWhere(wheres)` | Render WHERE clause |
57
+ | `renderOrderBy(orderBy)` | Render ORDER BY clause |
58
+ | `renderGroupBy(groupBy)` | Render GROUP BY clause |
59
+ | `renderHaving(having)` | Render HAVING clause |
60
+ | `renderJoins(joins)` | Render all JOIN clauses |
61
+ | `renderFrom(from)` | Render FROM clause source (table, subquery, union, CTE reference) |
62
+ | `needsLateral(join)` | Detect if JOIN needs LATERAL/CROSS APPLY |
63
+
64
+ ### Abstract Methods (implemented per dialect)
65
+
66
+ **DML:**
67
+ - `select(def: SelectQueryDef): QueryBuildResult`
68
+ - `insert(def: InsertQueryDef): QueryBuildResult`
69
+ - `insertIfNotExists(def: InsertIfNotExistsQueryDef): QueryBuildResult`
70
+ - `insertInto(def: InsertIntoQueryDef): QueryBuildResult`
71
+ - `update(def: UpdateQueryDef): QueryBuildResult`
72
+ - `delete(def: DeleteQueryDef): QueryBuildResult`
73
+ - `upsert(def: UpsertQueryDef): QueryBuildResult`
74
+
75
+ **DDL - Table:**
76
+ - `createTable(def)`, `dropTable(def)`, `renameTable(def)`, `truncate(def)`
77
+
78
+ **DDL - Column:**
79
+ - `addColumn(def)`, `dropColumn(def)`, `modifyColumn(def)`, `renameColumn(def)`
80
+
81
+ **DDL - Constraint:**
82
+ - `addPrimaryKey(def)`, `dropPrimaryKey(def)`, `addForeignKey(def)`, `dropForeignKey(def)`, `addIndex(def)`, `dropIndex(def)`
83
+
84
+ **DDL - View/Procedure:**
85
+ - `createView(def)`, `dropView(def)`, `createProc(def)`, `dropProc(def)`, `execProc(def)`
86
+
87
+ **Utils:**
88
+ - `clearSchema(def)`, `schemaExists(def)`, `switchFk(def)`
89
+
90
+ ---
91
+
92
+ ## ExprRendererBase
93
+
94
+ Abstract base class for `Expr`/`WhereExpr` to SQL rendering. Implements dispatch logic.
95
+
96
+ ```typescript
97
+ abstract class ExprRendererBase {
98
+ constructor(protected buildSelect: (def: SelectQueryDef) => string);
99
+
100
+ /** Render a single expression to SQL */
101
+ render(expr: Expr | WhereExpr): string;
102
+
103
+ /** Render multiple WHERE expressions joined with AND */
104
+ renderWhere(exprs: WhereExpr[]): string;
105
+
106
+ /** Wrap identifier (table/column name) */
107
+ abstract wrap(name: string): string;
108
+
109
+ /** Escape string value for SQL literals */
110
+ abstract escapeString(value: string): string;
111
+
112
+ /** Escape any value to SQL literal */
113
+ abstract escapeValue(value: unknown): string;
114
+ }
115
+ ```
116
+
117
+ ### Abstract Methods (per category)
118
+
119
+ **Value:** `column`, `value`, `raw`
120
+
121
+ **Comparison:** `eq`, `gt`, `lt`, `gte`, `lte`, `between`, `null`, `like`, `regexp`, `in`, `inQuery`, `exists`
122
+
123
+ **Logic:** `not`, `and`, `or`
124
+
125
+ **String:** `concat`, `left`, `right`, `trim`, `padStart`, `replace`, `upper`, `lower`, `length`, `byteLength`, `substring`, `indexOf`
126
+
127
+ **Number:** `abs`, `round`, `ceil`, `floor`
128
+
129
+ **Date:** `year`, `month`, `day`, `hour`, `minute`, `second`, `isoWeek`, `isoWeekStartDate`, `isoYearMonth`, `dateDiff`, `dateAdd`, `formatDate`
130
+
131
+ **Condition:** `coalesce`, `nullIf`, `is`, `switch`, `if`
132
+
133
+ **Aggregate:** `count`, `sum`, `avg`, `max`, `min`
134
+
135
+ **Other:** `greatest`, `least`, `rowNum`, `random`, `cast`
136
+
137
+ **Window:** `window`
138
+
139
+ **System:** `subquery`
140
+
141
+ ---
142
+
143
+ ## Dialect Implementations
144
+
145
+ ### MysqlQueryBuilder / MysqlExprRenderer
146
+
147
+ MySQL-specific implementation.
148
+
149
+ - Identifier wrapping: `` `name` ``
150
+ - NULL-safe equality: `<=>`
151
+ - LATERAL JOIN support
152
+ - UUID stored as `BINARY(16)`
153
+ - BOOLEAN mapped to `TINYINT(1)`
154
+
155
+ ### MssqlQueryBuilder / MssqlExprRenderer
156
+
157
+ Microsoft SQL Server-specific implementation.
158
+
159
+ - Identifier wrapping: `[name]`
160
+ - NULL-safe equality: `(source IS NULL AND target IS NULL) OR source = target`
161
+ - CROSS APPLY for lateral joins
162
+ - `TOP` and `OFFSET...FETCH` for pagination
163
+ - `IDENTITY_INSERT` for explicit AI column values
164
+ - UUID mapped to `UNIQUEIDENTIFIER`
165
+
166
+ ### PostgresqlQueryBuilder / PostgresqlExprRenderer
167
+
168
+ PostgreSQL-specific implementation.
169
+
170
+ - Identifier wrapping: `"name"`
171
+ - NULL-safe equality: `IS NOT DISTINCT FROM`
172
+ - LATERAL JOIN support
173
+ - `LIMIT...OFFSET` for pagination
174
+ - UUID as native `UUID` type
175
+ - `RETURN QUERY` in stored procedures
176
+
177
+ ---
178
+
179
+ ## Usage
180
+
181
+ Typically used internally by `DbContextExecutor` implementations. Direct usage:
182
+
183
+ ```typescript
184
+ import { createQueryBuilder } from "@simplysm/orm-common";
185
+
186
+ const builder = createQueryBuilder("mysql");
187
+
188
+ // Build from a Queryable
189
+ const qr = db.user()
190
+ .where((u) => [expr.eq(u.status, "active")])
191
+ .orderBy((u) => u.name);
192
+
193
+ const queryDef = qr.getSelectQueryDef();
194
+ const result = builder.build(queryDef);
195
+ // result.sql: "SELECT ... FROM `mydb`.`User` AS `T1` WHERE ..."
196
+ ```