@simplysm/orm-common 13.0.0-beta.28 → 13.0.0-beta.30

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 CHANGED
@@ -27,825 +27,54 @@ pnpm add @simplysm/orm-common
27
27
 
28
28
  ## Core Modules
29
29
 
30
- ### Schema Builders
30
+ ### Schema Definition
31
31
 
32
- | export | Description |
33
- |--------|------|
34
- | `Table(name)` | Table builder factory function |
35
- | `TableBuilder` | Table schema definition (Fluent API) |
36
- | `View(name)` | View builder factory function |
37
- | `ViewBuilder` | View schema definition (Fluent API) |
38
- | `Procedure(name)` | Procedure builder factory function |
39
- | `ProcedureBuilder` | Procedure schema definition (Fluent API) |
40
- | `ColumnBuilder` | Column definition builder |
41
- | `createColumnFactory()` | Create column type factory |
42
- | `IndexBuilder` | Index definition builder |
43
- | `createIndexFactory()` | Create index factory |
44
- | `ForeignKeyBuilder` | FK relationship builder (N:1, creates DB FK) |
45
- | `ForeignKeyTargetBuilder` | FK reverse reference builder (1:N) |
46
- | `RelationKeyBuilder` | Logical relationship builder (N:1, no DB FK) |
47
- | `RelationKeyTargetBuilder` | Logical reverse reference builder (1:N) |
48
- | `createRelationFactory()` | Create relation factory |
49
-
50
- ### Query Execution
51
-
52
- | export | Description |
53
- |--------|------|
54
- | `Queryable` | Query builder class (SELECT/INSERT/UPDATE/DELETE) |
55
- | `queryable(db, table)` | Create table Queryable from `DbContext` |
56
- | `Executable` | Procedure execution wrapper class |
57
- | `executable(db, proc)` | Create procedure Executable from `DbContext` |
58
- | `DbContext` | Database context abstract class (connection, transaction, DDL) |
59
-
60
- ### Expressions
61
-
62
- | export | Description |
63
- |--------|------|
64
- | `expr` | SQL expression builder object |
65
- | `toExpr(value)` | Convert `ExprInput` to `Expr` AST |
66
- | `ExprUnit` | Value expression wrapper class |
67
- | `WhereExprUnit` | WHERE condition expression wrapper class |
68
-
69
- ### Query Builder (SQL Generation)
70
-
71
- | export | Description |
72
- |--------|------|
73
- | `createQueryBuilder(dialect)` | Create dialect-specific query builder instance |
74
- | `QueryBuilderBase` | Query builder abstract base class |
75
- | `MysqlQueryBuilder` | MySQL SQL generator |
76
- | `MssqlQueryBuilder` | MSSQL SQL generator |
77
- | `PostgresqlQueryBuilder` | PostgreSQL SQL generator |
78
- | `ExprRendererBase` | Expression renderer abstract base class |
79
- | `MysqlExprRenderer` | MySQL expression renderer |
80
- | `MssqlExprRenderer` | MSSQL expression renderer |
81
- | `PostgresqlExprRenderer` | PostgreSQL expression renderer |
82
-
83
- ### Utilities
84
-
85
- | export | Description |
86
- |--------|------|
87
- | `parseSearchQuery(text)` | Parse search string into SQL LIKE patterns |
88
- | `parseQueryResult(rows, meta)` | Convert flat query results to nested objects |
89
- | `getMatchedPrimaryKeys(fk, table)` | Match FK columns with target table PK |
90
- | `SystemMigration` | Internal table for migration history management |
91
-
92
- ### Errors
93
-
94
- | export | Description |
95
- |--------|------|
96
- | `DbTransactionError` | Transaction error (DBMS independent) |
97
- | `DbErrorCode` | Error code enum (`NO_ACTIVE_TRANSACTION`, `DEADLOCK`, `LOCK_TIMEOUT`, etc.) |
98
-
99
- ### Types
100
-
101
- | export | Description |
102
- |--------|------|
103
- | `Dialect` | `"mysql" \| "mssql" \| "postgresql"` |
104
- | `dialects` | All Dialect array (for testing) |
105
- | `IsolationLevel` | Transaction isolation level |
106
- | `DbContextStatus` | `"ready" \| "connect" \| "transact"` |
107
- | `DbContextExecutor` | Query executor interface |
108
- | `Migration` | Migration definition interface |
109
- | `ResultMeta` | Query result conversion metadata |
110
- | `QueryBuildResult` | Built SQL + result set meta |
111
- | `DataRecord` | Query result record (supports recursive nesting) |
112
- | `DataType` | Column data type definition |
113
- | `ColumnPrimitive` | Column primitive type union |
114
- | `ColumnPrimitiveMap` | TypeScript type name to actual type mapping |
115
- | `ColumnPrimitiveStr` | Column primitive type name union (`"string" \| "number" \| ...`) |
116
- | `ColumnMeta` | Column metadata |
117
- | `ColumnBuilderRecord` | Record of column name to `ColumnBuilder` |
118
- | `InferColumns<T>` | Infer value types from column builder |
119
- | `InferInsertColumns<T>` | Infer types for INSERT (autoIncrement/nullable/default are optional) |
120
- | `InferUpdateColumns<T>` | Infer types for UPDATE (all fields optional) |
121
- | `InferColumnExprs<T>` | Infer expression input types from column builder |
122
- | `InferColumnPrimitiveFromDataType<T>` | Infer TypeScript type from `DataType` |
123
- | `DataToColumnBuilderRecord<T>` | Convert `DataRecord` to `ColumnBuilderRecord` |
124
- | `InferDeepRelations<T>` | Deep type inference from relation definitions |
125
- | `RelationBuilderRecord` | Record of relation name to relation builder |
126
- | `QueryableRecord<T>` | Queryable internal column record type |
127
- | `PathProxy<T>` | Type-safe path proxy for `include()` |
128
- | `ExprInput<T>` | Expression input type (`ExprUnit<T> \| T`) |
129
- | `SwitchExprBuilder<T>` | CASE WHEN builder (returned by `expr.switch()`) |
130
- | `ParsedSearchQuery` | Result of `parseSearchQuery()` (`{ or, must, not }`) |
131
- | `QueryDef` | Query definition union type (DML + DDL) |
132
- | `SelectQueryDef` | SELECT query definition |
133
- | `DDL_TYPES` | Array of all DDL query type strings |
134
- | `DdlType` | Union type of DDL query types |
135
- | `Expr`, `WhereExpr` | Expression AST types |
136
- | `DateSeparator` | Date part separator (`"year" \| "month" \| "day" \| ...`) |
137
- | `WinSpec` | Window function specification (`partitionBy`, `orderBy`) |
138
- | `dataTypeStrToColumnPrimitiveStr` | SQL type name to TypeScript type name mapping |
139
- | `inferColumnPrimitiveStr(value)` | Infer `ColumnPrimitiveStr` from runtime value |
140
-
141
- ## Usage
142
-
143
- ### Table Definition
144
-
145
- Define table schema using the `Table()` factory function and Fluent API.
146
-
147
- ```typescript
148
- import { Table } from "@simplysm/orm-common";
149
-
150
- const User = Table("User")
151
- .database("mydb")
152
- .columns((c) => ({
153
- id: c.bigint().autoIncrement(),
154
- name: c.varchar(100),
155
- email: c.varchar(200).nullable(),
156
- isActive: c.boolean().default(true),
157
- createdAt: c.datetime(),
158
- }))
159
- .primaryKey("id")
160
- .indexes((i) => [
161
- i.index("email").unique(),
162
- i.index("name", "createdAt").orderBy("ASC", "DESC"),
163
- ]);
164
- ```
165
-
166
- #### Column Types
167
-
168
- | Factory Method | SQL Type | TypeScript Type |
169
- |--------------|----------|----------------|
170
- | `c.int()` | INT | `number` |
171
- | `c.bigint()` | BIGINT | `number` |
172
- | `c.float()` | FLOAT | `number` |
173
- | `c.double()` | DOUBLE | `number` |
174
- | `c.decimal(p, s)` | DECIMAL(p, s) | `number` |
175
- | `c.varchar(n)` | VARCHAR(n) | `string` |
176
- | `c.char(n)` | CHAR(n) | `string` |
177
- | `c.text()` | TEXT | `string` |
178
- | `c.boolean()` | BOOLEAN / BIT / TINYINT(1) | `boolean` |
179
- | `c.datetime()` | DATETIME | `DateTime` |
180
- | `c.date()` | DATE | `DateOnly` |
181
- | `c.time()` | TIME | `Time` |
182
- | `c.uuid()` | UUID / UNIQUEIDENTIFIER / BINARY(16) | `Uuid` |
183
- | `c.binary()` | BLOB / VARBINARY(MAX) / BYTEA | `Bytes` |
184
-
185
- #### Column Options
186
-
187
- | Method | Description |
188
- |--------|------|
189
- | `.autoIncrement()` | Auto increment (optional on INSERT) |
190
- | `.nullable()` | Allow NULL (adds `undefined` to type) |
191
- | `.default(value)` | Set default value (optional on INSERT) |
192
- | `.description(text)` | Column description (DDL comment) |
193
-
194
- ### Relationship Definition
195
-
196
- Define relationships between tables to enable automatic JOINs via `include()`.
197
-
198
- ```typescript
199
- const Post = Table("Post")
200
- .database("mydb")
201
- .columns((c) => ({
202
- id: c.bigint().autoIncrement(),
203
- authorId: c.bigint(),
204
- title: c.varchar(200),
205
- content: c.text(),
206
- }))
207
- .primaryKey("id")
208
- .relations((r) => ({
209
- // N:1 relationship - Post.authorId → User.id (creates DB FK)
210
- author: r.foreignKey(["authorId"], () => User),
211
- }));
212
-
213
- const User = Table("User")
214
- .database("mydb")
215
- .columns((c) => ({
216
- id: c.bigint().autoIncrement(),
217
- name: c.varchar(100),
218
- }))
219
- .primaryKey("id")
220
- .relations((r) => ({
221
- // 1:N reverse reference - User ← Post.author
222
- posts: r.foreignKeyTarget(() => Post, "author"),
223
-
224
- // 1:1 relationship (single object)
225
- profile: r.foreignKeyTarget(() => Profile, "user").single(),
226
- }));
227
- ```
228
-
229
- #### Relationship Builder Types
230
-
231
- | Method | Cardinality | Creates DB FK | Available For |
232
- |--------|-----------|-----------|--------------|
233
- | `r.foreignKey(cols, targetFn)` | N:1 | Yes | Table |
234
- | `r.foreignKeyTarget(targetFn, relName)` | 1:N | - | Table |
235
- | `r.relationKey(cols, targetFn)` | N:1 | No | Table, View |
236
- | `r.relationKeyTarget(targetFn, relName)` | 1:N | - | Table, View |
237
-
238
- Calling `.single()` on `foreignKeyTarget` / `relationKeyTarget` establishes a 1:1 relationship (returns single object instead of array).
239
-
240
- ### DbContext Configuration
241
-
242
- Register tables and procedures by extending `DbContext`.
243
-
244
- ```typescript
245
- import { DbContext, queryable, executable, expr } from "@simplysm/orm-common";
246
-
247
- class MyDb extends DbContext {
248
- readonly user = queryable(this, User);
249
- readonly post = queryable(this, Post);
250
- readonly getUserById = executable(this, GetUserById);
251
-
252
- // Migration definitions
253
- readonly migrations = [
254
- {
255
- name: "20260101_add_status",
256
- up: async (db: MyDb) => {
257
- const c = createColumnFactory();
258
- await db.addColumn(
259
- { database: "mydb", name: "User" },
260
- "status",
261
- c.varchar(20).nullable(),
262
- );
263
- },
264
- },
265
- ];
266
- }
267
- ```
268
-
269
- ### Connection and Transactions
270
-
271
- ```typescript
272
- // executor is NodeDbContextExecutor from orm-node package, etc.
273
- const db = new MyDb(executor, { database: "mydb" });
274
-
275
- // Execute within transaction (auto commit/rollback)
276
- const users = await db.connect(async () => {
277
- const result = await db.user().result();
278
- await db.user().insert([{ name: "John Doe", createdAt: DateTime.now() }]);
279
- return result;
280
- });
281
-
282
- // Connect without transaction (for DDL operations)
283
- await db.connectWithoutTransaction(async () => {
284
- await db.initialize(); // Code First initialization
285
- });
286
-
287
- // Specify isolation level
288
- await db.connect(async () => {
289
- // ...
290
- }, "SERIALIZABLE");
291
- ```
292
-
293
- ### SELECT Queries
294
-
295
- ```typescript
296
- // Basic query
297
- const users = await db.user()
298
- .where((u) => [expr.eq(u.isActive, true)])
299
- .orderBy((u) => u.name)
300
- .result();
301
-
302
- // Column selection
303
- const names = await db.user()
304
- .select((u) => ({
305
- userName: u.name,
306
- userEmail: u.email,
307
- }))
308
- .result();
309
-
310
- // Single result (error if 2 or more)
311
- const user = await db.user()
312
- .where((u) => [expr.eq(u.id, 1)])
313
- .single();
314
-
315
- // First result only
316
- const latest = await db.user()
317
- .orderBy((u) => u.createdAt, "DESC")
318
- .first();
319
-
320
- // Row count
321
- const count = await db.user()
322
- .where((u) => [expr.eq(u.isActive, true)])
323
- .count();
324
-
325
- // Existence check
326
- const hasAdmin = await db.user()
327
- .where((u) => [expr.eq(u.role, "admin")])
328
- .exists();
329
- ```
330
-
331
- ### JOIN Queries
332
-
333
- ```typescript
334
- // Manual JOIN (1:N - array)
335
- const usersWithPosts = await db.user()
336
- .join("posts", (qr, u) =>
337
- qr.from(Post).where((p) => [expr.eq(p.authorId, u.id)])
338
- )
339
- .result();
340
- // Result: { id, name, posts: [{ id, title }, ...] }
341
-
342
- // Manual JOIN (N:1 - single object)
343
- const postsWithUser = await db.post()
344
- .joinSingle("author", (qr, p) =>
345
- qr.from(User).where((u) => [expr.eq(u.id, p.authorId)])
346
- )
347
- .result();
348
- // Result: { id, title, author: { id, name } | undefined }
349
-
350
- // include (auto JOIN based on relationship definition)
351
- const postWithAuthor = await db.post()
352
- .include((p) => p.author)
353
- .single();
354
-
355
- // Nested include
356
- const postWithAuthorCompany = await db.post()
357
- .include((p) => p.author.company)
358
- .result();
359
-
360
- // Multiple includes
361
- const userWithAll = await db.user()
362
- .include((u) => u.posts)
363
- .include((u) => u.profile)
364
- .result();
365
- ```
366
-
367
- ### Grouping and Aggregation
368
-
369
- ```typescript
370
- const stats = await db.order()
371
- .select((o) => ({
372
- userId: o.userId,
373
- totalAmount: expr.sum(o.amount),
374
- orderCount: expr.count(),
375
- }))
376
- .groupBy((o) => [o.userId])
377
- .having((o) => [expr.gte(o.totalAmount, 10000)])
378
- .result();
379
- ```
380
-
381
- ### Pagination
382
-
383
- ```typescript
384
- // TOP (no ORDER BY required)
385
- const topUsers = await db.user().top(10).result();
386
-
387
- // LIMIT/OFFSET (ORDER BY required)
388
- const page = await db.user()
389
- .orderBy((u) => u.createdAt, "DESC")
390
- .limit(0, 20) // skip 0, take 20
391
- .result();
392
- ```
393
-
394
- ### Text Search
395
-
396
- The `search()` method supports structured search syntax.
397
-
398
- ```typescript
399
- const users = await db.user()
400
- .search((u) => [u.name, u.email], "John -deleted")
401
- .result();
402
- ```
403
-
404
- #### Search Syntax
405
-
406
- | Syntax | Description | Example |
407
- |------|------|------|
408
- | Space | OR combination | `apple banana` |
409
- | `""` | Phrase search (required) | `"delicious apple"` |
410
- | `+` | Required (AND) | `+apple +banana` |
411
- | `-` | Exclude (NOT) | `apple -banana` |
412
- | `*` | Wildcard | `app*` |
413
- | `\*` | Escape | `app\*` (literal `*`) |
414
-
415
- ### UNION
416
-
417
- ```typescript
418
- const allItems = await Queryable.union(
419
- db.user()
420
- .where((u) => [expr.eq(u.type, "admin")])
421
- .select((u) => ({ name: u.name })),
422
- db.user()
423
- .where((u) => [expr.eq(u.type, "manager")])
424
- .select((u) => ({ name: u.name })),
425
- ).result();
426
- ```
427
-
428
- ### Subquery Wrapping (wrap)
429
-
430
- To use `count()` after `distinct()` or `groupBy()`, wrap the query with `wrap()`.
431
-
432
- ```typescript
433
- const count = await db.user()
434
- .select((u) => ({ name: u.name }))
435
- .distinct()
436
- .wrap()
437
- .count();
438
- ```
439
-
440
- ### Recursive CTE (recursive)
441
-
442
- Use for querying hierarchical data (org charts, category trees, etc.).
443
-
444
- ```typescript
445
- const employees = await db.employee()
446
- .where((e) => [expr.null(e.managerId)]) // Root node
447
- .recursive((cte) =>
448
- cte.from(Employee)
449
- .where((e) => [expr.eq(e.managerId, e.self[0].id)])
450
- )
451
- .result();
452
- ```
453
-
454
- ### INSERT
455
-
456
- ```typescript
457
- // Simple insert
458
- await db.user().insert([
459
- { name: "John Doe", createdAt: DateTime.now() },
460
- { name: "Jane Smith", createdAt: DateTime.now() },
461
- ]);
462
-
463
- // Insert with ID return (outputColumns)
464
- const [inserted] = await db.user().insert(
465
- [{ name: "John Doe", createdAt: DateTime.now() }],
466
- ["id"],
467
- );
468
- // inserted.id is available
469
-
470
- // Conditional insert (insert if not exists)
471
- await db.user()
472
- .where((u) => [expr.eq(u.email, "test@test.com")])
473
- .insertIfNotExists({ name: "Test", email: "test@test.com", createdAt: DateTime.now() });
474
-
475
- // INSERT INTO ... SELECT
476
- await db.user()
477
- .select((u) => ({ name: u.name, createdAt: u.createdAt }))
478
- .where((u) => [expr.eq(u.isArchived, false)])
479
- .insertInto(ArchivedUser);
480
- ```
481
-
482
- ### UPDATE
483
-
484
- ```typescript
485
- // Simple update
486
- await db.user()
487
- .where((u) => [expr.eq(u.id, 1)])
488
- .update((u) => ({
489
- name: expr.val("string", "New Name"),
490
- }));
491
-
492
- // Reference existing value
493
- await db.product()
494
- .update((p) => ({
495
- viewCount: expr.val("number", p.viewCount + 1),
496
- }));
497
- ```
498
-
499
- ### DELETE
500
-
501
- ```typescript
502
- // Simple delete
503
- await db.user()
504
- .where((u) => [expr.eq(u.id, 1)])
505
- .delete();
506
-
507
- // Return deleted data
508
- const deleted = await db.user()
509
- .where((u) => [expr.eq(u.isExpired, true)])
510
- .delete(["id", "name"]);
511
- ```
512
-
513
- ### UPSERT
514
-
515
- ```typescript
516
- // Same data for UPDATE/INSERT
517
- await db.user()
518
- .where((u) => [expr.eq(u.email, "test@test.com")])
519
- .upsert(() => ({
520
- name: expr.val("string", "Test"),
521
- email: expr.val("string", "test@test.com"),
522
- }));
523
-
524
- // Different data for UPDATE/INSERT
525
- await db.user()
526
- .where((u) => [expr.eq(u.email, "test@test.com")])
527
- .upsert(
528
- () => ({ loginCount: expr.val("number", 1) }),
529
- (update) => ({ ...update, email: expr.val("string", "test@test.com") }),
530
- );
531
- ```
532
-
533
- ### Row Locking (FOR UPDATE)
534
-
535
- ```typescript
536
- await db.connect(async () => {
537
- const user = await db.user()
538
- .where((u) => [expr.eq(u.id, 1)])
539
- .lock()
540
- .single();
541
- });
542
- ```
543
-
544
- ## Expressions (expr)
545
-
546
- The `expr` object generates Dialect-independent SQL expressions. It creates JSON AST instead of SQL strings, which the `QueryBuilder` converts for each DBMS.
547
-
548
- ### Comparison Expressions (WHERE)
549
-
550
- | Method | SQL | Description |
551
- |--------|-----|------|
552
- | `expr.eq(a, b)` | `a = b` (NULL-safe) | Equality comparison |
553
- | `expr.gt(a, b)` | `a > b` | Greater than |
554
- | `expr.lt(a, b)` | `a < b` | Less than |
555
- | `expr.gte(a, b)` | `a >= b` | Greater than or equal |
556
- | `expr.lte(a, b)` | `a <= b` | Less than or equal |
557
- | `expr.between(a, from, to)` | `a BETWEEN from AND to` | Range (unbounded in direction if undefined) |
558
- | `expr.null(a)` | `a IS NULL` | NULL check |
559
- | `expr.like(a, pattern)` | `a LIKE pattern` | Pattern matching |
560
- | `expr.regexp(a, pattern)` | `a REGEXP pattern` | Regex matching |
561
- | `expr.in(a, values)` | `a IN (v1, v2, ...)` | Value list comparison |
562
- | `expr.inQuery(a, query)` | `a IN (SELECT ...)` | Subquery comparison |
563
- | `expr.exists(query)` | `EXISTS (SELECT ...)` | Subquery existence |
564
-
565
- ### Logical Expressions (WHERE)
566
-
567
- | Method | SQL | Description |
568
- |--------|-----|------|
569
- | `expr.and(conditions)` | `(c1 AND c2 AND ...)` | All conditions met |
570
- | `expr.or(conditions)` | `(c1 OR c2 OR ...)` | At least one condition met |
571
- | `expr.not(condition)` | `NOT (condition)` | Negate condition |
572
-
573
- ### String Expressions
574
-
575
- | Method | SQL | Description |
576
- |--------|-----|------|
577
- | `expr.concat(...args)` | `CONCAT(a, b, ...)` | String concatenation |
578
- | `expr.left(s, n)` | `LEFT(s, n)` | Extract n chars from left |
579
- | `expr.right(s, n)` | `RIGHT(s, n)` | Extract n chars from right |
580
- | `expr.trim(s)` | `TRIM(s)` | Trim whitespace from both sides |
581
- | `expr.padStart(s, n, fill)` | `LPAD(s, n, fill)` | Left padding |
582
- | `expr.replace(s, from, to)` | `REPLACE(s, from, to)` | String replacement |
583
- | `expr.upper(s)` | `UPPER(s)` | Convert to uppercase |
584
- | `expr.lower(s)` | `LOWER(s)` | Convert to lowercase |
585
- | `expr.length(s)` | `CHAR_LENGTH(s)` | Character count |
586
- | `expr.byteLength(s)` | `OCTET_LENGTH(s)` | Byte count |
587
- | `expr.substring(s, start, len)` | `SUBSTRING(s, start, len)` | Substring extraction (1-based) |
588
- | `expr.indexOf(s, search)` | `LOCATE(search, s)` | Find position (1-based, 0 if not found) |
589
-
590
- ### Numeric Expressions
591
-
592
- | Method | SQL | Description |
593
- |--------|-----|------|
594
- | `expr.abs(n)` | `ABS(n)` | Absolute value |
595
- | `expr.round(n, digits)` | `ROUND(n, digits)` | Round |
596
- | `expr.ceil(n)` | `CEILING(n)` | Ceiling |
597
- | `expr.floor(n)` | `FLOOR(n)` | Floor |
598
-
599
- ### Date Expressions
600
-
601
- | Method | SQL | Description |
602
- |--------|-----|------|
603
- | `expr.year(d)` | `YEAR(d)` | Extract year |
604
- | `expr.month(d)` | `MONTH(d)` | Extract month (1~12) |
605
- | `expr.day(d)` | `DAY(d)` | Extract day (1~31) |
606
- | `expr.hour(d)` | `HOUR(d)` | Extract hour (0~23) |
607
- | `expr.minute(d)` | `MINUTE(d)` | Extract minute (0~59) |
608
- | `expr.second(d)` | `SECOND(d)` | Extract second (0~59) |
609
- | `expr.isoWeek(d)` | `WEEK(d, 3)` | ISO week (1~53) |
610
- | `expr.isoWeekStartDate(d)` | - | ISO week start date (Monday) |
611
- | `expr.isoYearMonth(d)` | - | First day of the month |
612
- | `expr.dateDiff(sep, from, to)` | `DATEDIFF(sep, from, to)` | Date difference |
613
- | `expr.dateAdd(sep, source, value)` | `DATEADD(sep, value, source)` | Add to date |
614
- | `expr.formatDate(d, format)` | `DATE_FORMAT(d, format)` | Date formatting |
615
-
616
- `DateSeparator`: `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`
617
-
618
- ### Conditional Expressions
619
-
620
- | Method | SQL | Description |
621
- |--------|-----|------|
622
- | `expr.ifNull(a, b, ...)` | `COALESCE(a, b, ...)` | Return first non-null value |
623
- | `expr.nullIf(a, b)` | `NULLIF(a, b)` | NULL if `a === b` |
624
- | `expr.is(condition)` | `(condition)` | Convert WHERE to boolean |
625
- | `expr.if(cond, then, else)` | `IF(cond, then, else)` | Ternary condition |
626
- | `expr.switch()` | `CASE WHEN ... END` | Multiple condition branching |
627
-
628
- ```typescript
629
- // CASE WHEN usage example
630
- db.user().select((u) => ({
631
- grade: expr.switch<string>()
632
- .case(expr.gte(u.score, 90), "A")
633
- .case(expr.gte(u.score, 80), "B")
634
- .case(expr.gte(u.score, 70), "C")
635
- .default("F"),
636
- }))
637
- ```
638
-
639
- ### Aggregate Expressions
640
-
641
- | Method | SQL | Description |
642
- |--------|-----|------|
643
- | `expr.count(col?, distinct?)` | `COUNT(*)` / `COUNT(DISTINCT col)` | Row count |
644
- | `expr.sum(col)` | `SUM(col)` | Sum |
645
- | `expr.avg(col)` | `AVG(col)` | Average |
646
- | `expr.max(col)` | `MAX(col)` | Maximum |
647
- | `expr.min(col)` | `MIN(col)` | Minimum |
648
- | `expr.greatest(...args)` | `GREATEST(a, b, ...)` | Greatest among multiple values |
649
- | `expr.least(...args)` | `LEAST(a, b, ...)` | Least among multiple values |
650
-
651
- ### Window Functions
652
-
653
- | Method | SQL | Description |
654
- |--------|-----|------|
655
- | `expr.rowNumber(spec)` | `ROW_NUMBER() OVER (...)` | Row number |
656
- | `expr.rank(spec)` | `RANK() OVER (...)` | Rank (gaps on ties) |
657
- | `expr.denseRank(spec)` | `DENSE_RANK() OVER (...)` | Dense rank (consecutive) |
658
- | `expr.ntile(n, spec)` | `NTILE(n) OVER (...)` | Split into n groups |
659
- | `expr.lag(col, spec, opts?)` | `LAG(col, offset) OVER (...)` | Previous row value |
660
- | `expr.lead(col, spec, opts?)` | `LEAD(col, offset) OVER (...)` | Next row value |
661
- | `expr.firstValue(col, spec)` | `FIRST_VALUE(col) OVER (...)` | First value |
662
- | `expr.lastValue(col, spec)` | `LAST_VALUE(col) OVER (...)` | Last value |
663
- | `expr.sumOver(col, spec)` | `SUM(col) OVER (...)` | Window sum |
664
- | `expr.avgOver(col, spec)` | `AVG(col) OVER (...)` | Window average |
665
- | `expr.countOver(spec, col?)` | `COUNT(*) OVER (...)` | Window count |
666
- | `expr.minOver(col, spec)` | `MIN(col) OVER (...)` | Window minimum |
667
- | `expr.maxOver(col, spec)` | `MAX(col) OVER (...)` | Window maximum |
668
-
669
- `WinSpec`: `{ partitionBy?: [...], orderBy?: [[col, "ASC"|"DESC"], ...] }`
670
-
671
- ```typescript
672
- // Window function usage example
673
- db.order().select((o) => ({
674
- ...o,
675
- rowNum: expr.rowNumber({
676
- partitionBy: [o.userId],
677
- orderBy: [[o.createdAt, "DESC"]],
678
- }),
679
- runningTotal: expr.sumOver(o.amount, {
680
- partitionBy: [o.userId],
681
- orderBy: [[o.createdAt, "ASC"]],
682
- }),
683
- }))
684
- ```
685
-
686
- ### Other Expressions
687
-
688
- | Method | SQL | Description |
689
- |--------|-----|------|
690
- | `expr.val(dataType, value)` | Literal | Wrap typed value |
691
- | `expr.col(dataType, ...path)` | Column reference | Create column reference (internal) |
692
- | `expr.raw(dataType)\`sql\`` | Raw SQL | Escape hatch for DBMS-specific functions |
693
- | `expr.rowNum()` | - | Total row number |
694
- | `expr.random()` | `RAND()` / `RANDOM()` | Random number 0~1 |
695
- | `expr.cast(source, type)` | `CAST(source AS type)` | Type conversion |
696
- | `expr.subquery(dataType, qr)` | `(SELECT ...)` | Scalar subquery |
697
-
698
- ```typescript
699
- // Raw SQL (using DBMS-specific functions)
700
- db.user().select((u) => ({
701
- name: u.name,
702
- data: expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`,
703
- }))
704
-
705
- // Scalar subquery
706
- db.user().select((u) => ({
707
- id: u.id,
708
- postCount: expr.subquery(
709
- "number",
710
- db.post()
711
- .where((p) => [expr.eq(p.userId, u.id)])
712
- .select(() => ({ cnt: expr.count() }))
713
- ),
714
- }))
715
- ```
716
-
717
- ## View Definition
718
-
719
- ```typescript
720
- import { View, expr } from "@simplysm/orm-common";
32
+ See [docs/schema.md](docs/schema.md) for full documentation.
721
33
 
722
- const ActiveUsers = View("ActiveUsers")
723
- .database("mydb")
724
- .query((db: MyDb) =>
725
- db.user()
726
- .where((u) => [expr.eq(u.isActive, true)])
727
- .select((u) => ({
728
- id: u.id,
729
- name: u.name,
730
- email: u.email,
731
- }))
732
- );
733
-
734
- // Define logical relationships on views (no DB FK)
735
- const UserSummary = View("UserSummary")
736
- .database("mydb")
737
- .query((db: MyDb) =>
738
- db.user().select((u) => ({
739
- id: u.id,
740
- name: u.name,
741
- companyId: u.companyId,
742
- }))
743
- )
744
- .relations((r) => ({
745
- company: r.relationKey(["companyId"], () => Company),
746
- }));
747
- ```
748
-
749
- ## Procedure Definition
750
-
751
- ```typescript
752
- import { Procedure, executable } from "@simplysm/orm-common";
753
-
754
- const GetUserById = Procedure("GetUserById")
755
- .database("mydb")
756
- .params((c) => ({
757
- userId: c.bigint(),
758
- }))
759
- .returns((c) => ({
760
- id: c.bigint(),
761
- name: c.varchar(100),
762
- email: c.varchar(200),
763
- }))
764
- .body("SELECT id, name, email FROM User WHERE id = userId");
765
-
766
- // Register in DbContext
767
- class MyDb extends DbContext {
768
- readonly getUserById = executable(this, GetUserById);
769
- }
770
-
771
- // Invoke
772
- const result = await db.getUserById().execute({ userId: 1 });
773
- ```
774
-
775
- ## Query Builder (SQL Generation)
776
-
777
- Converts `QueryDef` JSON AST to DBMS-specific SQL strings.
778
-
779
- ```typescript
780
- import { createQueryBuilder } from "@simplysm/orm-common";
781
-
782
- const mysqlBuilder = createQueryBuilder("mysql");
783
- const mssqlBuilder = createQueryBuilder("mssql");
784
- const postgresqlBuilder = createQueryBuilder("postgresql");
785
-
786
- // Convert QueryDef to SQL
787
- const queryDef = db.user()
788
- .where((u) => [expr.eq(u.isActive, true)])
789
- .getSelectQueryDef();
790
-
791
- const { sql } = mysqlBuilder.build(queryDef);
792
- ```
793
-
794
- ## DDL Operations
34
+ - **[Table(name)](docs/schema.md#table-definition)** - Table builder factory function
35
+ - **[View(name)](docs/schema.md#view-definition)** - View builder factory function
36
+ - **[Procedure(name)](docs/schema.md#procedure-definition)** - Procedure builder factory function
37
+ - **[Column Types](docs/schema.md#column-types)** - `c.int()`, `c.varchar()`, `c.datetime()`, etc.
38
+ - **[Column Options](docs/schema.md#column-options)** - `.nullable()`, `.autoIncrement()`, `.default()`, `.description()`
39
+ - **[Relationships](docs/schema.md#relationship-definition)** - `r.foreignKey()`, `r.foreignKeyTarget()`, `r.relationKey()`, `r.relationKeyTarget()`
40
+ - **[DbContext](docs/schema.md#dbcontext-configuration)** - Database context class for connection, transactions, and migrations
41
+ - **[Type Inference](docs/schema.md#type-inference)** - `$infer`, `$inferInsert`, `$inferUpdate`
795
42
 
796
- `DbContext` supports Code First DDL operations.
797
-
798
- ```typescript
799
- await db.connectWithoutTransaction(async () => {
800
- // Initialize database (create tables/views/procedures/FKs/indexes)
801
- await db.initialize();
802
-
803
- // Force initialize (drop and recreate existing data)
804
- await db.initialize({ force: true });
805
-
806
- // Individual DDL operations
807
- const c = createColumnFactory();
808
- await db.addColumn({ database: "mydb", name: "User" }, "status", c.varchar(20).nullable());
809
- await db.modifyColumn({ database: "mydb", name: "User" }, "status", c.varchar(50).nullable());
810
- await db.renameColumn({ database: "mydb", name: "User" }, "status", "userStatus");
811
- await db.dropColumn({ database: "mydb", name: "User" }, "userStatus");
812
-
813
- await db.renameTable({ database: "mydb", name: "User" }, "Member");
814
- await db.truncate({ database: "mydb", name: "User" });
815
- });
816
- ```
817
-
818
- ## Error Handling
819
-
820
- ```typescript
821
- import { DbTransactionError, DbErrorCode } from "@simplysm/orm-common";
822
-
823
- try {
824
- await db.connect(async () => {
825
- // ...
826
- });
827
- } catch (err) {
828
- if (err instanceof DbTransactionError) {
829
- switch (err.code) {
830
- case DbErrorCode.DEADLOCK:
831
- // Deadlock retry logic
832
- break;
833
- case DbErrorCode.LOCK_TIMEOUT:
834
- // Timeout handling
835
- break;
836
- }
837
- }
838
- }
839
- ```
840
-
841
- ### DbErrorCode
43
+ ### Query Execution
842
44
 
843
- | Code | Description |
844
- |------|------|
845
- | `NO_ACTIVE_TRANSACTION` | No active transaction |
846
- | `TRANSACTION_ALREADY_STARTED` | Transaction already started |
847
- | `DEADLOCK` | Deadlock occurred |
848
- | `LOCK_TIMEOUT` | Lock timeout |
45
+ See [docs/queries.md](docs/queries.md) for full documentation.
46
+
47
+ - **[Connection & Transactions](docs/queries.md#connection-and-transactions)** - `db.connect()`, `db.connectWithoutTransaction()`
48
+ - **[SELECT Queries](docs/queries.md#select-queries)** - `.where()`, `.select()`, `.single()`, `.first()`, `.result()`, `.count()`, `.exists()`
49
+ - **[JOIN Queries](docs/queries.md#join-queries)** - `.join()`, `.joinSingle()`, `.include()`
50
+ - **[Grouping & Aggregation](docs/queries.md#grouping-and-aggregation)** - `.groupBy()`, `.having()`
51
+ - **[Pagination](docs/queries.md#pagination)** - `.top()`, `.limit()`
52
+ - **[Text Search](docs/queries.md#text-search)** - `.search()` with structured search syntax
53
+ - **[UNION](docs/queries.md#union)** - `Queryable.union()`
54
+ - **[Subquery Wrapping](docs/queries.md#subquery-wrapping-wrap)** - `.wrap()`
55
+ - **[Recursive CTE](docs/queries.md#recursive-cte-recursive)** - `.recursive()`
56
+ - **[INSERT](docs/queries.md#insert)** - `.insert()`, `.insertIfNotExists()`, `.insertInto()`
57
+ - **[UPDATE](docs/queries.md#update)** - `.update()`
58
+ - **[DELETE](docs/queries.md#delete)** - `.delete()`
59
+ - **[UPSERT](docs/queries.md#upsert)** - `.upsert()`
60
+ - **[Row Locking](docs/queries.md#row-locking-for-update)** - `.lock()` (FOR UPDATE)
61
+ - **[DDL Operations](docs/queries.md#ddl-operations)** - `db.initialize()`, `db.addColumn()`, `db.modifyColumn()`, etc.
62
+ - **[Query Builder](docs/queries.md#query-builder-sql-generation)** - `createQueryBuilder()` for converting QueryDef to SQL
63
+ - **[Error Handling](docs/queries.md#error-handling)** - `DbTransactionError`, `DbErrorCode`
64
+
65
+ ### SQL Expressions
66
+
67
+ See [docs/expressions.md](docs/expressions.md) for full documentation.
68
+
69
+ - **[Comparison Expressions](docs/expressions.md#comparison-expressions-where)** - `expr.eq()`, `expr.gt()`, `expr.lt()`, `expr.between()`, `expr.in()`, `expr.like()`, `expr.regexp()`, `expr.exists()`
70
+ - **[Logical Expressions](docs/expressions.md#logical-expressions-where)** - `expr.and()`, `expr.or()`, `expr.not()`
71
+ - **[String Expressions](docs/expressions.md#string-expressions)** - `expr.concat()`, `expr.trim()`, `expr.substring()`, `expr.upper()`, `expr.lower()`, `expr.length()`
72
+ - **[Numeric Expressions](docs/expressions.md#numeric-expressions)** - `expr.abs()`, `expr.round()`, `expr.ceil()`, `expr.floor()`
73
+ - **[Date Expressions](docs/expressions.md#date-expressions)** - `expr.year()`, `expr.month()`, `expr.day()`, `expr.dateDiff()`, `expr.dateAdd()`, `expr.formatDate()`
74
+ - **[Conditional Expressions](docs/expressions.md#conditional-expressions)** - `expr.ifNull()`, `expr.nullIf()`, `expr.if()`, `expr.switch()`
75
+ - **[Aggregate Expressions](docs/expressions.md#aggregate-expressions)** - `expr.count()`, `expr.sum()`, `expr.avg()`, `expr.max()`, `expr.min()`, `expr.greatest()`, `expr.least()`
76
+ - **[Window Functions](docs/expressions.md#window-functions)** - `expr.rowNumber()`, `expr.rank()`, `expr.denseRank()`, `expr.lag()`, `expr.lead()`, `expr.sumOver()`, `expr.avgOver()`
77
+ - **[Other Expressions](docs/expressions.md#other-expressions)** - `expr.val()`, `expr.raw()`, `expr.cast()`, `expr.subquery()`, `expr.random()`
849
78
 
850
79
  ## Security Notes
851
80
 
@@ -867,31 +96,47 @@ if (Number.isNaN(userId)) throw new Error("Invalid ID");
867
96
  await db.user().where((u) => [expr.eq(u.id, userId)]).result();
868
97
  ```
869
98
 
870
- ## Type Inference
871
-
872
- `TableBuilder` automatically infers types from column definitions.
99
+ ## Quick Start
873
100
 
874
101
  ```typescript
102
+ import { Table, DbContext, queryable, expr } from "@simplysm/orm-common";
103
+
104
+ // Define table schema
875
105
  const User = Table("User")
106
+ .database("mydb")
876
107
  .columns((c) => ({
877
108
  id: c.bigint().autoIncrement(),
878
109
  name: c.varchar(100),
879
110
  email: c.varchar(200).nullable(),
880
- status: c.varchar(20).default("active"),
111
+ createdAt: c.datetime(),
881
112
  }))
882
113
  .primaryKey("id");
883
114
 
884
- // $infer: Full type (columns + relations)
885
- type UserData = typeof User.$infer;
886
- // { id: number; name: string; email: string | undefined; status: string; }
115
+ // Create DbContext
116
+ class MyDb extends DbContext {
117
+ readonly user = queryable(this, User);
118
+ }
119
+
120
+ // Use with executor (from orm-node package)
121
+ const db = new MyDb(executor, { database: "mydb" });
122
+
123
+ // Execute queries
124
+ await db.connect(async () => {
125
+ // INSERT
126
+ await db.user().insert([
127
+ { name: "John", createdAt: DateTime.now() }
128
+ ]);
887
129
 
888
- // $inferInsert: For INSERT (autoIncrement/nullable/default are optional)
889
- type UserInsert = typeof User.$inferInsert;
890
- // { name: string; } & { id?: number; email?: string; status?: string; }
130
+ // SELECT
131
+ const users = await db.user()
132
+ .where((u) => [expr.eq(u.email, "john@example.com")])
133
+ .result();
891
134
 
892
- // $inferUpdate: For UPDATE (all fields optional)
893
- type UserUpdate = typeof User.$inferUpdate;
894
- // { id?: number; name?: string; email?: string; status?: string; }
135
+ // UPDATE
136
+ await db.user()
137
+ .where((u) => [expr.eq(u.id, 1)])
138
+ .update(() => ({ name: expr.val("string", "Jane") }));
139
+ });
895
140
  ```
896
141
 
897
142
  ## License