@simplysm/orm-common 13.0.42 → 13.0.43

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.
Files changed (2) hide show
  1. package/README.md +870 -168
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -21,65 +21,15 @@ pnpm add @simplysm/orm-common
21
21
 
22
22
  ## Main Modules
23
23
 
24
- ### Schema Definition
24
+ ### Core
25
25
 
26
- See [docs/schema.md](docs/schema.md) for full documentation.
27
-
28
- - **[Table(name)](docs/schema.md#table-definition)** - Table builder factory function
29
- - **[View(name)](docs/schema.md#view-definition)** - View builder factory function
30
- - **[Procedure(name)](docs/schema.md#procedure-definition)** - Procedure builder factory function
31
- - **[Column Types](docs/schema.md#column-types)** - `c.int()`, `c.varchar()`, `c.datetime()`, etc.
32
- - **[Column Options](docs/schema.md#column-options)** - `.nullable()`, `.autoIncrement()`, `.default()`, `.description()`
33
- - **[Relationships](docs/schema.md#relationship-definition)** - `r.foreignKey()`, `r.foreignKeyTarget()`, `r.relationKey()`, `r.relationKeyTarget()`
34
- - **[DbContext](docs/schema.md#dbcontext-configuration)** - Database context class for connection, transactions, and migrations
35
- - **[Type Inference](docs/schema.md#type-inference)** - `$infer`, `$inferInsert`, `$inferUpdate`
36
-
37
- ### Query Execution
38
-
39
- See [docs/queries.md](docs/queries.md) for full documentation.
40
-
41
- - **[Connection & Transactions](docs/queries.md#connection-and-transactions)** - `db.connect()`, `db.connectWithoutTransaction()`, `db.trans()`
42
- - **[SELECT Queries](docs/queries.md#select-queries)** - `.where()`, `.select()`, `.distinct()`, `.orderBy()`, `.single()`, `.first()`, `.result()`, `.count()`, `.exists()`
43
- - **[JOIN Queries](docs/queries.md#join-queries)** - `.join()`, `.joinSingle()`, `.include()`
44
- - **[Grouping & Aggregation](docs/queries.md#grouping-and-aggregation)** - `.groupBy()`, `.having()`
45
- - **[Pagination](docs/queries.md#pagination)** - `.top()`, `.limit()`
46
- - **[Text Search](docs/queries.md#text-search)** - `.search()` with structured search syntax
47
- - **[UNION](docs/queries.md#union)** - `Queryable.union()`
48
- - **[Subquery Wrapping](docs/queries.md#subquery-wrapping-wrap)** - `.wrap()`
49
- - **[Recursive CTE](docs/queries.md#recursive-cte-recursive)** - `.recursive()`
50
- - **[INSERT](docs/queries.md#insert)** - `.insert()`, `.insertIfNotExists()`, `.insertInto()`
51
- - **[UPDATE](docs/queries.md#update)** - `.update()`
52
- - **[DELETE](docs/queries.md#delete)** - `.delete()`
53
- - **[UPSERT](docs/queries.md#upsert)** - `.upsert()`
54
- - **[Row Locking](docs/queries.md#row-locking-for-update)** - `.lock()` (FOR UPDATE)
55
- - **[DDL Operations](docs/queries.md#ddl-operations)** - `db.initialize()`, `db.addColumn()`, `db.modifyColumn()`, etc.
56
- - **[Query Builder](docs/queries.md#query-builder-sql-generation)** - `createQueryBuilder()` for converting QueryDef to SQL
57
- - **[Error Handling](docs/queries.md#error-handling)** - `DbTransactionError`, `DbErrorCode`
58
-
59
- ### SQL Expressions
60
-
61
- See [docs/expressions.md](docs/expressions.md) for full documentation.
62
-
63
- - **[Comparison Expressions](docs/expressions.md#comparison-expressions-where)** - `expr.eq()`, `expr.gt()`, `expr.lt()`, `expr.between()`, `expr.in()`, `expr.like()`, `expr.regexp()`, `expr.exists()`
64
- - **[Logical Expressions](docs/expressions.md#logical-expressions-where)** - `expr.and()`, `expr.or()`, `expr.not()`
65
- - **[String Expressions](docs/expressions.md#string-expressions)** - `expr.concat()`, `expr.trim()`, `expr.substring()`, `expr.upper()`, `expr.lower()`, `expr.length()`
66
- - **[Numeric Expressions](docs/expressions.md#numeric-expressions)** - `expr.abs()`, `expr.round()`, `expr.ceil()`, `expr.floor()`
67
- - **[Date Expressions](docs/expressions.md#date-expressions)** - `expr.year()`, `expr.month()`, `expr.day()`, `expr.dateDiff()`, `expr.dateAdd()`, `expr.formatDate()`
68
- - **[Conditional Expressions](docs/expressions.md#conditional-expressions)** - `expr.ifNull()`, `expr.nullIf()`, `expr.if()`, `expr.switch()`
69
- - **[Aggregate Expressions](docs/expressions.md#aggregate-expressions)** - `expr.count()`, `expr.sum()`, `expr.avg()`, `expr.max()`, `expr.min()`, `expr.greatest()`, `expr.least()`
70
- - **[Window Functions](docs/expressions.md#window-functions)** - `expr.rowNumber()`, `expr.rank()`, `expr.denseRank()`, `expr.ntile()`, `expr.lag()`, `expr.lead()`, `expr.firstValue()`, `expr.lastValue()`, `expr.sumOver()`, `expr.avgOver()`, `expr.countOver()`, `expr.minOver()`, `expr.maxOver()`
71
- - **[Other Expressions](docs/expressions.md#other-expressions)** - `expr.val()`, `expr.raw()`, `expr.cast()`, `expr.subquery()`, `expr.random()`
72
-
73
- ## DbContext API
74
-
75
- ### Functional API (Recommended)
26
+ #### `defineDbContext(config)`
76
27
 
77
- The functional API uses `defineDbContext` + `createDbContext` for better type safety and composability.
28
+ Creates a `DbContextDef` schema blueprint without any runtime state.
78
29
 
79
30
  ```typescript
80
- import { defineDbContext, createDbContext, createColumnFactory } from "@simplysm/orm-common";
31
+ import { defineDbContext } from "@simplysm/orm-common";
81
32
 
82
- // Step 1: Define DbContext schema
83
33
  const MyDbDef = defineDbContext({
84
34
  tables: { user: User, post: Post },
85
35
  views: { activeUsers: ActiveUsers },
@@ -98,34 +48,33 @@ const MyDbDef = defineDbContext({
98
48
  },
99
49
  ],
100
50
  });
101
-
102
- // Step 2: Create instance with executor
103
- const db = createDbContext(MyDbDef, executor, { database: "mydb" });
104
-
105
- // Use queryable accessors
106
- await db.connect(async () => {
107
- const users = await db.user().result();
108
- const posts = await db.post().result();
109
- });
110
51
  ```
111
52
 
112
- #### `defineDbContext(config)`
113
-
114
- Creates a `DbContextDef` schema blueprint without any runtime state.
115
-
116
53
  | Parameter | Type | Description |
117
54
  |-----------|------|-------------|
118
55
  | `config.tables` | `Record<string, TableBuilder>` | Table builders |
119
- | `config.views` | `Record<string, ViewBuilder>` | View builders |
120
- | `config.procedures` | `Record<string, ProcedureBuilder>` | Procedure builders |
121
- | `config.migrations` | `Migration[]` | Migration list |
56
+ | `config.views` | `Record<string, ViewBuilder>` | View builders (optional) |
57
+ | `config.procedures` | `Record<string, ProcedureBuilder>` | Procedure builders (optional) |
58
+ | `config.migrations` | `Migration[]` | Migration list (optional) |
122
59
 
123
60
  Returns `DbContextDef<TTables, TViews, TProcedures>`.
124
61
 
62
+ Note: `defineDbContext` automatically adds an internal `_Migration` system table to `tables` to track applied migrations.
63
+
125
64
  #### `createDbContext(def, executor, opt)`
126
65
 
127
66
  Creates a fully functional `DbContextInstance` from a `DbContextDef`.
128
67
 
68
+ ```typescript
69
+ import { createDbContext } from "@simplysm/orm-common";
70
+
71
+ const db = createDbContext(MyDbDef, executor, { database: "mydb" });
72
+
73
+ await db.connect(async () => {
74
+ const users = await db.user().result();
75
+ });
76
+ ```
77
+
129
78
  | Parameter | Type | Description |
130
79
  |-----------|------|-------------|
131
80
  | `def` | `DbContextDef` | Schema definition from `defineDbContext()` |
@@ -146,87 +95,792 @@ Returns `DbContextInstance<TDef>`.
146
95
  | `DbContextDdlMethods` | Interface for all DDL methods and QueryDef generator methods |
147
96
  | `DbContextStatus` | `"ready" \| "connect" \| "transact"` — current connection status |
148
97
 
149
- ### Class-based API (Removed)
98
+ #### `DbTransactionError`
150
99
 
151
- The old class-based API has been removed. You must migrate to the functional API for better type safety and composability.
100
+ Database transaction error class. Wraps DBMS-native errors with a standardized `DbErrorCode`.
152
101
 
153
102
  ```typescript
154
- // Old (no longer available):
155
- // import { DbContext, queryable } from "@simplysm/orm-common";
156
- // class MyDb extends DbContext { ... } // This is no longer supported
157
-
158
- // New (required):
159
- const MyDbDef = defineDbContext({
160
- tables: { user: User },
161
- migrations: [...],
162
- });
163
- const db = createDbContext(MyDbDef, executor, { database: "mydb" });
103
+ import { DbTransactionError, DbErrorCode } from "@simplysm/orm-common";
104
+
105
+ try {
106
+ await executor.rollbackTransaction();
107
+ } catch (err) {
108
+ if (err instanceof DbTransactionError) {
109
+ if (err.code === DbErrorCode.NO_ACTIVE_TRANSACTION) {
110
+ return; // Already rolled back, ignore
111
+ }
112
+ }
113
+ throw err;
114
+ }
164
115
  ```
165
116
 
166
- ### Migration Guide
117
+ | `DbErrorCode` value | Description |
118
+ |---------------------|-------------|
119
+ | `NO_ACTIVE_TRANSACTION` | No active transaction (rollback when none exists) |
120
+ | `TRANSACTION_ALREADY_STARTED` | Transaction already started |
121
+ | `DEADLOCK` | Deadlock detected |
122
+ | `LOCK_TIMEOUT` | Lock wait timeout |
123
+
124
+ `DbTransactionError` properties:
125
+
126
+ | Property | Type | Description |
127
+ |----------|------|-------------|
128
+ | `code` | `DbErrorCode` | Standardized error code |
129
+ | `message` | `string` | Error message |
130
+ | `originalError` | `unknown?` | Original DBMS error (for debugging) |
131
+
132
+ ---
133
+
134
+ ### DbContext Instance API
167
135
 
168
- To migrate from class-based to functional API:
136
+ The `DbContextInstance` returned by `createDbContext` exposes the following methods:
169
137
 
170
- **Step 1: Replace class definition**
138
+ #### Connection Management
139
+
140
+ | Method | Description |
141
+ |--------|-------------|
142
+ | `connect(fn, isolationLevel?)` | Open connection → begin transaction → run callback → commit → close. Rolls back on error. |
143
+ | `connectWithoutTransaction(fn)` | Open connection → run callback → close. No transaction. Use for DDL or read-only operations. |
144
+ | `trans(fn, isolationLevel?)` | Start a transaction within an already-connected context. Use inside `connectWithoutTransaction` when partial transactions are needed. |
171
145
 
172
146
  ```typescript
173
- // Before:
174
- class MyDb extends DbContext {
175
- readonly user = queryable(this, User);
176
- readonly post = queryable(this, Post);
177
- readonly getUserById = executable(this, GetUserById);
147
+ // Default: with transaction
148
+ await db.connect(async () => {
149
+ await db.user().insert([{ name: "Alice" }]);
150
+ await db.post().insert([{ title: "Hello", userId: 1 }]);
151
+ });
178
152
 
179
- readonly migrations = [
180
- { name: "...", up: async (db: MyDb) => { ... } }
181
- ];
182
- }
153
+ // Without transaction (e.g., for DDL)
154
+ await db.connectWithoutTransaction(async () => {
155
+ await db.createTable(User);
156
+ });
183
157
 
184
- // After:
185
- const MyDbDef = defineDbContext({
186
- tables: { user: User, post: Post },
187
- procedures: { getUserById: GetUserById },
188
- migrations: [
189
- { name: "...", up: async (db) => { ... } }
190
- ],
158
+ // Nested transaction inside connectWithoutTransaction
159
+ await db.connectWithoutTransaction(async () => {
160
+ const report = await db.report().result();
161
+ await db.trans(async () => {
162
+ await db.log().insert([{ action: "read" }]);
163
+ });
191
164
  });
192
165
  ```
193
166
 
194
- **Step 2: Replace instantiation**
167
+ #### Queryable / Executable Accessors
168
+
169
+ Each table/view/procedure defined in `defineDbContext` becomes an accessor method on the instance:
195
170
 
196
171
  ```typescript
197
- // Before:
198
- const db = new MyDb(executor, { database: "mydb" });
172
+ // Tables and views → returns () => Queryable
173
+ db.user() // Queryable<UserData, typeof User>
174
+ db.post() // Queryable<PostData, typeof Post>
199
175
 
200
- // After:
201
- const db = createDbContext(MyDbDef, executor, { database: "mydb" });
176
+ // Procedures → returns () => Executable
177
+ db.getUserById() // Executable<Params, Returns>
202
178
  ```
203
179
 
204
- **Step 3: Update usage (no changes needed)**
180
+ #### `initialize(options?)`
181
+
182
+ Applies all pending migrations in order.
205
183
 
206
184
  ```typescript
207
- // Both APIs use the same queryable accessors:
208
- await db.connect(async () => {
209
- const users = await db.user().result(); // Same syntax
185
+ await db.connectWithoutTransaction(async () => {
186
+ await db.initialize();
210
187
  });
211
188
  ```
212
189
 
213
- **Type inference:**
190
+ | Option | Type | Description |
191
+ |--------|------|-------------|
192
+ | `options.dbs` | `string[]?` | Restrict migration to specific databases |
193
+ | `options.force` | `boolean?` | Re-apply all migrations even if already applied |
194
+
195
+ #### DDL Methods
196
+
197
+ All DDL methods are available on the `DbContextInstance`. DDL cannot be called inside a `connect()` (transact) context — use `connectWithoutTransaction` instead.
198
+
199
+ | Method | Description |
200
+ |--------|-------------|
201
+ | `createTable(table)` | Create a table based on `TableBuilder` |
202
+ | `dropTable(table)` | Drop a table |
203
+ | `renameTable(table, newName)` | Rename a table |
204
+ | `createView(view)` | Create a view |
205
+ | `dropView(view)` | Drop a view |
206
+ | `createProc(procedure)` | Create a stored procedure |
207
+ | `dropProc(procedure)` | Drop a stored procedure |
208
+ | `addColumn(table, columnName, column)` | Add a column |
209
+ | `dropColumn(table, column)` | Drop a column |
210
+ | `modifyColumn(table, columnName, column)` | Modify a column definition |
211
+ | `renameColumn(table, column, newName)` | Rename a column |
212
+ | `addPk(table, columns)` | Add a primary key |
213
+ | `dropPk(table)` | Drop the primary key |
214
+ | `addFk(table, relationName, relationDef)` | Add a foreign key |
215
+ | `dropFk(table, relationName)` | Drop a foreign key |
216
+ | `addIdx(table, indexBuilder)` | Add an index |
217
+ | `dropIdx(table, columns)` | Drop an index |
218
+ | `clearSchema(params)` | Drop all objects in a schema |
219
+ | `schemaExists(database, schema?)` | Check if a schema/database exists |
220
+ | `truncate(table)` | Truncate a table |
221
+ | `switchFk(table, "on" \| "off")` | Toggle FK constraint checking |
222
+
223
+ DDL `QueryDef` generator equivalents (return `QueryDef` without executing):
224
+
225
+ | Method | Description |
226
+ |--------|-------------|
227
+ | `getCreateTableQueryDef(table)` | |
228
+ | `getCreateViewQueryDef(view)` | |
229
+ | `getCreateProcQueryDef(procedure)` | |
230
+ | `getCreateObjectQueryDef(builder)` | Unified for table, view, or procedure |
231
+ | `getDropTableQueryDef(table)` | |
232
+ | `getRenameTableQueryDef(table, newName)` | |
233
+ | `getDropViewQueryDef(view)` | |
234
+ | `getDropProcQueryDef(procedure)` | |
235
+ | `getAddColumnQueryDef(table, columnName, column)` | |
236
+ | `getDropColumnQueryDef(table, column)` | |
237
+ | `getModifyColumnQueryDef(table, columnName, column)` | |
238
+ | `getRenameColumnQueryDef(table, column, newName)` | |
239
+ | `getAddPkQueryDef(table, columns)` | |
240
+ | `getDropPkQueryDef(table)` | |
241
+ | `getAddFkQueryDef(table, relationName, relationDef)` | |
242
+ | `getAddIdxQueryDef(table, indexBuilder)` | |
243
+ | `getDropFkQueryDef(table, relationName)` | |
244
+ | `getDropIdxQueryDef(table, columns)` | |
245
+ | `getClearSchemaQueryDef(params)` | |
246
+ | `getSchemaExistsQueryDef(database, schema?)` | |
247
+ | `getTruncateQueryDef(table)` | |
248
+ | `getSwitchFkQueryDef(table, switch_)` | |
249
+
250
+ ---
251
+
252
+ ### Queryable / Executable
253
+
254
+ #### `Queryable<TData, TFrom>`
255
+
256
+ The main query builder class. All methods are immutable and return a new `Queryable`.
257
+
258
+ ```typescript
259
+ import { Queryable } from "@simplysm/orm-common";
260
+ ```
261
+
262
+ **Query building methods (return `Queryable`):**
263
+
264
+ | Method | SQL | Notes |
265
+ |--------|-----|-------|
266
+ | `.select(fn)` | SELECT | Map columns to new shape |
267
+ | `.distinct()` | DISTINCT | Remove duplicate rows |
268
+ | `.lock()` | FOR UPDATE | Row-level exclusive lock (must be inside `connect`) |
269
+ | `.top(count)` | TOP / LIMIT | First N rows |
270
+ | `.limit(skip, take)` | OFFSET / FETCH | Pagination — requires prior `orderBy()` |
271
+ | `.orderBy(fn, dir?)` | ORDER BY | Add sort; multiple calls chain in order |
272
+ | `.where(predicate)` | WHERE | Add condition; multiple calls AND together |
273
+ | `.search(fn, searchText)` | WHERE LIKE | Multi-column text search with special syntax |
274
+ | `.groupBy(fn)` | GROUP BY | Group rows |
275
+ | `.having(predicate)` | HAVING | Filter groups |
276
+ | `.join(as, fwd)` | LEFT JOIN | 1:N relationship → result as array property |
277
+ | `.joinSingle(as, fwd)` | LEFT JOIN | N:1 / 1:1 → result as single object property |
278
+ | `.include(fn)` | LEFT JOIN | Auto-join using `TableBuilder` relation definitions |
279
+ | `.wrap()` | Subquery | Wrap current query as subquery |
280
+ | `.recursive(fwd)` | WITH RECURSIVE | Recursive CTE for hierarchical data |
281
+
282
+ **Static method:**
283
+
284
+ | Method | Description |
285
+ |--------|-------------|
286
+ | `Queryable.union(...queries)` | UNION of 2+ queryables (deduplication) |
287
+
288
+ **Execution methods:**
289
+
290
+ | Method | Returns | Description |
291
+ |--------|---------|-------------|
292
+ | `.result()` | `Promise<TData[]>` | Execute SELECT, return all rows |
293
+ | `.single()` | `Promise<TData \| undefined>` | Return one row; throws if more than one |
294
+ | `.first()` | `Promise<TData \| undefined>` | Return first row only |
295
+ | `.count(fwd?)` | `Promise<number>` | Count rows; cannot use after `distinct()`/`groupBy()` without `wrap()` |
296
+ | `.exists()` | `Promise<boolean>` | Check if any rows match |
297
+ | `.insert(records, outputColumns?)` | `Promise<void \| Pick[]>` | INSERT; returns output if columns specified; auto-chunks at 1000 |
298
+ | `.insertIfNotExists(record, outputColumns?)` | `Promise<void \| Pick>` | INSERT if WHERE condition not matched |
299
+ | `.insertInto(targetTable, outputColumns?)` | `Promise<void \| Pick[]>` | INSERT INTO ... SELECT |
300
+ | `.update(recordFwd, outputColumns?)` | `Promise<void \| Pick[]>` | UPDATE |
301
+ | `.delete(outputColumns?)` | `Promise<void \| Pick[]>` | DELETE |
302
+ | `.upsert(updateFwd, insertFwd?, outputColumns?)` | `Promise<void \| Pick[]>` | UPDATE or INSERT |
303
+ | `.switchFk("on" \| "off")` | `Promise<void>` | Toggle FK for this table |
304
+
305
+ **QueryDef generator methods (for custom execution):**
306
+
307
+ | Method | Description |
308
+ |--------|-------------|
309
+ | `.getSelectQueryDef()` | Build SELECT `QueryDef` |
310
+ | `.getInsertQueryDef(records, outputColumns?)` | Build INSERT `QueryDef` |
311
+ | `.getInsertIfNotExistsQueryDef(record, outputColumns?)` | Build INSERT IF NOT EXISTS `QueryDef` |
312
+ | `.getInsertIntoQueryDef(targetTable, outputColumns?)` | Build INSERT INTO SELECT `QueryDef` |
313
+ | `.getUpdateQueryDef(recordFwd, outputColumns?)` | Build UPDATE `QueryDef` |
314
+ | `.getDeleteQueryDef(outputColumns?)` | Build DELETE `QueryDef` |
315
+ | `.getUpsertQueryDef(updateFwd, insertFwd, outputColumns?)` | Build UPSERT `QueryDef` |
316
+ | `.getResultMeta(outputColumns?)` | Build `ResultMeta` for result parsing |
317
+
318
+ **Examples:**
214
319
 
215
320
  ```typescript
216
- // Extract instance type:
217
- type MyDb = DbContextInstance<typeof MyDbDef>;
321
+ // SELECT with WHERE and ORDER BY
322
+ const users = await db.user()
323
+ .where((u) => [expr.eq(u.isActive, true)])
324
+ .orderBy((u) => u.name)
325
+ .result();
326
+
327
+ // JOIN (1:N)
328
+ const usersWithPosts = await db.user()
329
+ .join("posts", (qr, u) =>
330
+ qr.from(Post).where((p) => [expr.eq(p.userId, u.id)])
331
+ )
332
+ .result();
333
+ // Result: { id, name, posts?: Post[] }
334
+
335
+ // INCLUDE (using relation definitions)
336
+ const posts = await db.post()
337
+ .include((p) => p.author)
338
+ .result();
339
+
340
+ // Pagination
341
+ const page = await db.user()
342
+ .orderBy((u) => u.id)
343
+ .limit(0, 20)
344
+ .result();
345
+
346
+ // INSERT and get returned ID
347
+ const [inserted] = await db.user().insert(
348
+ [{ name: "Alice" }],
349
+ ["id"],
350
+ );
351
+
352
+ // UPSERT
353
+ await db.user()
354
+ .where((u) => [expr.eq(u.email, "alice@example.com")])
355
+ .upsert(
356
+ () => ({ loginCount: expr.val("number", 1) }),
357
+ (update) => ({ ...update, email: expr.val("string", "alice@example.com") }),
358
+ );
359
+
360
+ // Recursive CTE (hierarchical query)
361
+ const tree = await db.employee()
362
+ .where((e) => [expr.null(e.managerId)])
363
+ .recursive((cte) =>
364
+ cte.from(Employee)
365
+ .where((e) => [expr.eq(e.managerId, e.self[0].id)])
366
+ )
367
+ .result();
368
+ ```
218
369
 
219
- // Use in function parameters:
220
- async function doSomething(db: MyDb) {
221
- await db.user().result();
370
+ #### `Executable<TParams, TReturns>`
371
+
372
+ Wrapper class for stored procedure execution.
373
+
374
+ ```typescript
375
+ import { Executable } from "@simplysm/orm-common";
376
+ ```
377
+
378
+ | Method | Returns | Description |
379
+ |--------|---------|-------------|
380
+ | `.execute(params)` | `Promise<InferColumnExprs<TReturns>[][]>` | Execute the stored procedure |
381
+ | `.getExecProcQueryDef(params?)` | `QueryDef` | Build EXEC PROC `QueryDef` |
382
+
383
+ ```typescript
384
+ const result = await db.getUserById().execute({ userId: 1 });
385
+ ```
386
+
387
+ ---
388
+
389
+ ### Schema Builders
390
+
391
+ #### `Table(name)` / `TableBuilder`
392
+
393
+ Factory function and builder class for defining database tables.
394
+
395
+ ```typescript
396
+ import { Table } from "@simplysm/orm-common";
397
+
398
+ const User = Table("User")
399
+ .database("mydb")
400
+ .columns((c) => ({
401
+ id: c.bigint().autoIncrement(),
402
+ name: c.varchar(100),
403
+ email: c.varchar(200).nullable(),
404
+ status: c.varchar(20).default("active"),
405
+ createdAt: c.datetime(),
406
+ }))
407
+ .primaryKey("id")
408
+ .indexes((i) => [
409
+ i.index("email").unique(),
410
+ i.index("name", "createdAt").orderBy("ASC", "DESC"),
411
+ ])
412
+ .relations((r) => ({
413
+ posts: r.foreignKeyTarget(() => Post, "author"),
414
+ profile: r.foreignKeyTarget(() => Profile, "user").single(),
415
+ }));
416
+ ```
417
+
418
+ `TableBuilder` fluent methods:
419
+
420
+ | Method | Description |
421
+ |--------|-------------|
422
+ | `.database(db)` | Set database name |
423
+ | `.schema(schema)` | Set schema name (MSSQL/PostgreSQL) |
424
+ | `.description(desc)` | Set table description (DDL comment) |
425
+ | `.columns(fn)` | Define columns via column factory |
426
+ | `.primaryKey(...columns)` | Set primary key (supports composite PK) |
427
+ | `.indexes(fn)` | Define indexes via index factory |
428
+ | `.relations(fn)` | Define relationships via relation factory |
429
+
430
+ `TableBuilder` type inference properties:
431
+
432
+ | Property | Description |
433
+ |----------|-------------|
434
+ | `$infer` | Full row type (columns + relations) |
435
+ | `$inferColumns` | Columns-only type |
436
+ | `$inferInsert` | INSERT type (required fields required, autoIncrement/nullable/default optional) |
437
+ | `$inferUpdate` | UPDATE type (all fields optional) |
438
+ | `$columns` | Raw `ColumnBuilderRecord` |
439
+ | `$relations` | Raw `RelationBuilderRecord` |
440
+
441
+ #### `View(name)` / `ViewBuilder`
442
+
443
+ Factory function and builder class for defining database views.
444
+
445
+ ```typescript
446
+ import { View } from "@simplysm/orm-common";
447
+
448
+ const ActiveUsers = View("ActiveUsers")
449
+ .database("mydb")
450
+ .query((db: MyDb) =>
451
+ db.user()
452
+ .where((u) => [expr.eq(u.status, "active")])
453
+ .select((u) => ({ id: u.id, name: u.name }))
454
+ );
455
+ ```
456
+
457
+ `ViewBuilder` fluent methods:
458
+
459
+ | Method | Description |
460
+ |--------|-------------|
461
+ | `.database(db)` | Set database name |
462
+ | `.schema(schema)` | Set schema name |
463
+ | `.description(desc)` | Set view description |
464
+ | `.query(viewFn)` | Define the view SELECT query; takes a `DbContextBase` and returns `Queryable` |
465
+ | `.relations(fn)` | Define relationships (only `relationKey`/`relationKeyTarget`, not FK) |
466
+
467
+ `ViewBuilder` type inference properties:
468
+
469
+ | Property | Description |
470
+ |----------|-------------|
471
+ | `$infer` | View row type |
472
+ | `$relations` | Raw `RelationBuilderRecord` |
473
+
474
+ #### `Procedure(name)` / `ProcedureBuilder`
475
+
476
+ Factory function and builder class for defining stored procedures.
477
+
478
+ ```typescript
479
+ import { Procedure } from "@simplysm/orm-common";
480
+
481
+ const GetUserById = Procedure("GetUserById")
482
+ .database("mydb")
483
+ .params((c) => ({
484
+ userId: c.bigint(),
485
+ }))
486
+ .returns((c) => ({
487
+ id: c.bigint(),
488
+ name: c.varchar(100),
489
+ email: c.varchar(200).nullable(),
490
+ }))
491
+ .body("SELECT id, name, email FROM User WHERE id = userId");
492
+ ```
493
+
494
+ `ProcedureBuilder` fluent methods:
495
+
496
+ | Method | Description |
497
+ |--------|-------------|
498
+ | `.database(db)` | Set database name |
499
+ | `.schema(schema)` | Set schema name |
500
+ | `.description(desc)` | Set procedure description |
501
+ | `.params(fn)` | Define input parameters |
502
+ | `.returns(fn)` | Define result columns |
503
+ | `.body(sql)` | Set procedure body SQL (DBMS-specific syntax) |
504
+
505
+ `ProcedureBuilder` type inference properties:
506
+
507
+ | Property | Description |
508
+ |----------|-------------|
509
+ | `$params` | Raw parameter `ColumnBuilderRecord` |
510
+ | `$returns` | Raw returns `ColumnBuilderRecord` |
511
+
512
+ ### Column Types (`createColumnFactory`)
513
+
514
+ Used inside `.columns()`, `.params()`, and `.returns()`, and also directly in migrations.
515
+
516
+ ```typescript
517
+ import { createColumnFactory } from "@simplysm/orm-common";
518
+
519
+ const c = createColumnFactory();
520
+ // Used in migrations:
521
+ await db.addColumn({ database: "mydb", name: "User" }, "status", c.varchar(20).nullable());
522
+ ```
523
+
524
+ Available column type methods:
525
+
526
+ | Method | SQL Type | TypeScript Type |
527
+ |--------|----------|-----------------|
528
+ | `c.int()` | INT | `number` |
529
+ | `c.bigint()` | BIGINT | `number` |
530
+ | `c.float()` | FLOAT | `number` |
531
+ | `c.double()` | DOUBLE | `number` |
532
+ | `c.decimal(precision, scale?)` | DECIMAL | `number` |
533
+ | `c.varchar(length)` | VARCHAR | `string` |
534
+ | `c.char(length)` | CHAR | `string` |
535
+ | `c.text()` | TEXT | `string` |
536
+ | `c.binary()` | LONGBLOB/VARBINARY(MAX)/BYTEA | `Bytes` (Uint8Array) |
537
+ | `c.boolean()` | TINYINT(1)/BIT/BOOLEAN | `boolean` |
538
+ | `c.datetime()` | DATETIME | `DateTime` |
539
+ | `c.date()` | DATE | `DateOnly` |
540
+ | `c.time()` | TIME | `Time` |
541
+ | `c.uuid()` | BINARY(16)/UNIQUEIDENTIFIER/UUID | `Uuid` |
542
+
543
+ `ColumnBuilder` modifier methods:
544
+
545
+ | Method | Description |
546
+ |--------|-------------|
547
+ | `.autoIncrement()` | Auto-increment; excluded from INSERT required fields |
548
+ | `.nullable()` | Allow NULL; adds `undefined` to the TypeScript type |
549
+ | `.default(value)` | Default value; makes the field optional in INSERT |
550
+ | `.description(desc)` | Column description (DDL comment) |
551
+
552
+ ### Relation Builders
553
+
554
+ Used inside `.relations()`:
555
+
556
+ #### `r.foreignKey(columns, targetFn)` → `ForeignKeyBuilder`
557
+
558
+ N:1 relationship with DB FK constraint.
559
+
560
+ ```typescript
561
+ const Post = Table("Post")
562
+ .columns((c) => ({ id: c.bigint().autoIncrement(), authorId: c.bigint() }))
563
+ .primaryKey("id")
564
+ .relations((r) => ({
565
+ author: r.foreignKey(["authorId"], () => User),
566
+ }));
567
+ ```
568
+
569
+ #### `r.foreignKeyTarget(targetTableFn, relationName)` → `ForeignKeyTargetBuilder`
570
+
571
+ 1:N reverse reference. Results in an array by default; call `.single()` for 1:1.
572
+
573
+ ```typescript
574
+ const User = Table("User")
575
+ .columns((c) => ({ id: c.bigint().autoIncrement() }))
576
+ .primaryKey("id")
577
+ .relations((r) => ({
578
+ posts: r.foreignKeyTarget(() => Post, "author"), // 1:N → Post[]
579
+ profile: r.foreignKeyTarget(() => Profile, "user").single(), // 1:1 → Profile
580
+ }));
581
+ ```
582
+
583
+ #### `r.relationKey(columns, targetFn)` → `RelationKeyBuilder`
584
+
585
+ N:1 logical relationship without a DB FK constraint. Available for both tables and views.
586
+
587
+ #### `r.relationKeyTarget(targetTableFn, relationName)` → `RelationKeyTargetBuilder`
588
+
589
+ 1:N logical reverse reference without a DB FK constraint. Call `.single()` for 1:1.
590
+
591
+ #### `IndexBuilder` / `createIndexFactory`
592
+
593
+ Index builder, used inside `.indexes()`.
594
+
595
+ ```typescript
596
+ Table("User")
597
+ .indexes((i) => [
598
+ i.index("email").unique(),
599
+ i.index("name").name("IX_User_Name"),
600
+ i.index("createdAt").orderBy("DESC"),
601
+ i.index("status", "createdAt").orderBy("ASC", "DESC"),
602
+ ]);
603
+ ```
604
+
605
+ `IndexBuilder` methods:
606
+
607
+ | Method | Description |
608
+ |--------|-------------|
609
+ | `.unique()` | Create a unique index |
610
+ | `.name(name)` | Set index name |
611
+ | `.orderBy(...dirs)` | Set sort direction per column (`"ASC"` or `"DESC"`) |
612
+ | `.description(desc)` | Index description |
613
+
614
+ ---
615
+
616
+ ### SQL Expressions (`expr`)
617
+
618
+ The `expr` object provides all SQL expression builders. All methods return `ExprUnit` (SELECT expressions) or `WhereExprUnit` (WHERE conditions).
619
+
620
+ ```typescript
621
+ import { expr } from "@simplysm/orm-common";
622
+ ```
623
+
624
+ #### Value / Column Creation
625
+
626
+ | Method | Description |
627
+ |--------|-------------|
628
+ | `expr.val(dataType, value)` | Wrap a literal value as `ExprUnit` |
629
+ | `expr.col(dataType, ...path)` | Create a column reference (internal use) |
630
+ | `expr.raw(dataType)\`SQL ${interpolation}\`` | Raw SQL with interpolated `ExprUnit` values |
631
+
632
+ ```typescript
633
+ expr.val("string", "active")
634
+ expr.val("number", 100)
635
+ expr.val("DateOnly", DateOnly.today())
636
+ expr.raw("number")`ARRAY_LENGTH(${u.tags}, 1)`
637
+ ```
638
+
639
+ #### Comparison Expressions (WHERE)
640
+
641
+ | Method | SQL | Description |
642
+ |--------|-----|-------------|
643
+ | `expr.eq(source, target)` | `<=>` / IS NULL | NULL-safe equality |
644
+ | `expr.gt(source, target)` | `>` | Greater than |
645
+ | `expr.lt(source, target)` | `<` | Less than |
646
+ | `expr.gte(source, target)` | `>=` | Greater than or equal |
647
+ | `expr.lte(source, target)` | `<=` | Less than or equal |
648
+ | `expr.between(source, from?, to?)` | `BETWEEN` | Range; `undefined` means unbounded |
649
+ | `expr.null(source)` | `IS NULL` | Null check |
650
+ | `expr.like(source, pattern)` | `LIKE` | Pattern matching (`%`, `_`) |
651
+ | `expr.regexp(source, pattern)` | `REGEXP` | Regular expression matching |
652
+ | `expr.in(source, values)` | `IN (...)` | Value list match |
653
+ | `expr.inQuery(source, query)` | `IN (SELECT ...)` | Subquery match (single-column SELECT) |
654
+ | `expr.exists(query)` | `EXISTS (...)` | Subquery existence check |
655
+
656
+ ```typescript
657
+ db.user()
658
+ .where((u) => [
659
+ expr.eq(u.status, "active"),
660
+ expr.gte(u.age, 18),
661
+ expr.between(u.score, 0, 100),
662
+ expr.null(u.deletedAt),
663
+ expr.in(u.role, ["admin", "manager"]),
664
+ expr.like(u.name, "John%"),
665
+ ])
666
+ ```
667
+
668
+ #### Logical Expressions (WHERE)
669
+
670
+ | Method | SQL | Description |
671
+ |--------|-----|-------------|
672
+ | `expr.not(arg)` | `NOT (...)` | Negate a condition |
673
+ | `expr.and(conditions)` | `(... AND ...)` | All conditions must be true |
674
+ | `expr.or(conditions)` | `(... OR ...)` | At least one condition must be true |
675
+
676
+ #### String Expressions
677
+
678
+ | Method | SQL | Description |
679
+ |--------|-----|-------------|
680
+ | `expr.concat(...args)` | `CONCAT(...)` | Concatenate strings; NULL treated as empty string |
681
+ | `expr.left(source, length)` | `LEFT(...)` | Extract N characters from left |
682
+ | `expr.right(source, length)` | `RIGHT(...)` | Extract N characters from right |
683
+ | `expr.trim(source)` | `TRIM(...)` | Remove surrounding whitespace |
684
+ | `expr.padStart(source, length, fillString)` | `LPAD(...)` | Left-pad to target length |
685
+ | `expr.replace(source, from, to)` | `REPLACE(...)` | Replace all occurrences |
686
+ | `expr.upper(source)` | `UPPER(...)` | Uppercase |
687
+ | `expr.lower(source)` | `LOWER(...)` | Lowercase |
688
+ | `expr.length(source)` | `CHAR_LENGTH(...)` | Character length |
689
+ | `expr.byteLength(source)` | `OCTET_LENGTH(...)` | Byte length (UTF-8: Korean = 3 bytes) |
690
+ | `expr.substring(source, start, length?)` | `SUBSTRING(...)` | Extract substring (1-based index) |
691
+ | `expr.indexOf(source, search)` | `LOCATE(...)` | Find position (1-based; 0 if not found) |
692
+
693
+ #### Numeric Expressions
694
+
695
+ | Method | SQL | Description |
696
+ |--------|-----|-------------|
697
+ | `expr.abs(source)` | `ABS(...)` | Absolute value |
698
+ | `expr.round(source, digits)` | `ROUND(...)` | Round to N decimal places |
699
+ | `expr.ceil(source)` | `CEILING(...)` | Round up |
700
+ | `expr.floor(source)` | `FLOOR(...)` | Round down |
701
+
702
+ #### Date Expressions
703
+
704
+ | Method | SQL | Description |
705
+ |--------|-----|-------------|
706
+ | `expr.year(source)` | `YEAR(...)` | Extract year (4-digit) |
707
+ | `expr.month(source)` | `MONTH(...)` | Extract month (1–12) |
708
+ | `expr.day(source)` | `DAY(...)` | Extract day (1–31) |
709
+ | `expr.hour(source)` | `HOUR(...)` | Extract hour (0–23) |
710
+ | `expr.minute(source)` | `MINUTE(...)` | Extract minute (0–59) |
711
+ | `expr.second(source)` | `SECOND(...)` | Extract second (0–59) |
712
+ | `expr.isoWeek(source)` | `WEEK(..., 3)` | ISO 8601 week number (1–53, Mon start) |
713
+ | `expr.isoWeekStartDate(source)` | — | Monday of the week containing the date |
714
+ | `expr.isoYearMonth(source)` | — | First day of the month (YYYY-MM-01) |
715
+ | `expr.dateDiff(separator, from, to)` | `DATEDIFF(...)` | Date difference in specified unit |
716
+ | `expr.dateAdd(separator, source, value)` | `DATEADD(...)` | Add an interval to a date |
717
+ | `expr.formatDate(source, format)` | `DATE_FORMAT(...)` | Format date as string (DBMS-specific format strings) |
718
+
719
+ `DateSeparator` values: `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`
720
+
721
+ #### Conditional Expressions
722
+
723
+ | Method | SQL | Description |
724
+ |--------|-----|-------------|
725
+ | `expr.ifNull(...args)` | `COALESCE(...)` | Return first non-null value |
726
+ | `expr.nullIf(source, value)` | `NULLIF(...)` | Return NULL if source equals value |
727
+ | `expr.is(condition)` | — | Convert WHERE expression to boolean `ExprUnit` |
728
+ | `expr.if(condition, then, else_)` | `IF(...)` / `CASE` | Ternary conditional |
729
+ | `expr.switch<T>().case(...).default(...)` | `CASE WHEN ... END` | Multi-branch conditional |
730
+
731
+ ```typescript
732
+ // COALESCE
733
+ expr.ifNull(u.nickname, u.name, "Guest")
734
+
735
+ // Ternary
736
+ expr.if(expr.gte(u.age, 18), "adult", "minor")
737
+
738
+ // CASE WHEN
739
+ expr.switch<string>()
740
+ .case(expr.gte(u.score, 90), "A")
741
+ .case(expr.gte(u.score, 80), "B")
742
+ .default("F")
743
+
744
+ // Boolean from condition
745
+ db.user().select((u) => ({
746
+ isActive: expr.is(expr.eq(u.status, "active")),
747
+ }))
748
+ ```
749
+
750
+ #### Aggregate Expressions
751
+
752
+ | Method | SQL | Description |
753
+ |--------|-----|-------------|
754
+ | `expr.count(arg?, distinct?)` | `COUNT(...)` | Row count; omit arg for `COUNT(*)` |
755
+ | `expr.sum(arg)` | `SUM(...)` | Sum; NULL if all values are NULL |
756
+ | `expr.avg(arg)` | `AVG(...)` | Average; NULL if all values are NULL |
757
+ | `expr.max(arg)` | `MAX(...)` | Maximum; NULL if all values are NULL |
758
+ | `expr.min(arg)` | `MIN(...)` | Minimum; NULL if all values are NULL |
759
+ | `expr.greatest(...args)` | `GREATEST(...)` | Largest among multiple values |
760
+ | `expr.least(...args)` | `LEAST(...)` | Smallest among multiple values |
761
+
762
+ #### Window Functions
763
+
764
+ All window functions accept a `WinSpecInput`:
765
+
766
+ ```typescript
767
+ interface WinSpecInput {
768
+ partitionBy?: ExprInput<ColumnPrimitive>[];
769
+ orderBy?: [ExprInput<ColumnPrimitive>, ("ASC" | "DESC")?][];
222
770
  }
223
771
  ```
224
772
 
225
- ## Low-Level Utilities
773
+ | Method | SQL | Description |
774
+ |--------|-----|-------------|
775
+ | `expr.rowNumber(spec)` | `ROW_NUMBER() OVER (...)` | Sequential row number within partition |
776
+ | `expr.rank(spec)` | `RANK() OVER (...)` | Rank; ties get same rank, next skipped |
777
+ | `expr.denseRank(spec)` | `DENSE_RANK() OVER (...)` | Dense rank; ties get same rank, no skipping |
778
+ | `expr.ntile(n, spec)` | `NTILE(n) OVER (...)` | Split partition into N buckets |
779
+ | `expr.lag(column, spec, options?)` | `LAG(...) OVER (...)` | Previous row value |
780
+ | `expr.lead(column, spec, options?)` | `LEAD(...) OVER (...)` | Next row value |
781
+ | `expr.firstValue(column, spec)` | `FIRST_VALUE(...) OVER (...)` | First value in window |
782
+ | `expr.lastValue(column, spec)` | `LAST_VALUE(...) OVER (...)` | Last value in window |
783
+ | `expr.sumOver(column, spec)` | `SUM(...) OVER (...)` | Cumulative/windowed sum |
784
+ | `expr.avgOver(column, spec)` | `AVG(...) OVER (...)` | Windowed average |
785
+ | `expr.countOver(spec, column?)` | `COUNT(*) OVER (...)` | Windowed row count |
786
+ | `expr.minOver(column, spec)` | `MIN(...) OVER (...)` | Windowed minimum |
787
+ | `expr.maxOver(column, spec)` | `MAX(...) OVER (...)` | Windowed maximum |
788
+
789
+ ```typescript
790
+ db.order().select((o) => ({
791
+ ...o,
792
+ rowNum: expr.rowNumber({
793
+ partitionBy: [o.userId],
794
+ orderBy: [[o.createdAt, "DESC"]],
795
+ }),
796
+ runningTotal: expr.sumOver(o.amount, {
797
+ partitionBy: [o.userId],
798
+ orderBy: [[o.createdAt, "ASC"]],
799
+ }),
800
+ }))
801
+ ```
802
+
803
+ #### Other Expressions
804
+
805
+ | Method | SQL | Description |
806
+ |--------|-----|-------------|
807
+ | `expr.cast(source, targetType)` | `CAST(... AS ...)` | Type conversion |
808
+ | `expr.subquery(dataType, queryable)` | `(SELECT ...)` | Scalar subquery in SELECT |
809
+ | `expr.random()` | `RAND()` / `RANDOM()` | Random number 0–1 |
810
+ | `expr.rowNum()` | — | Row number without window spec |
811
+ | `expr.toExpr(value)` | — | Convert `ExprInput` to raw `Expr` AST |
812
+
813
+ ```typescript
814
+ // CAST
815
+ expr.cast(o.id, { type: "varchar", length: 20 })
816
+
817
+ // Scalar subquery
818
+ expr.subquery(
819
+ "number",
820
+ db.post().where((p) => [expr.eq(p.userId, u.id)]).select(() => ({ cnt: expr.count() }))
821
+ )
822
+
823
+ // Random sort
824
+ db.user().orderBy(() => expr.random()).limit(0, 10)
825
+ ```
826
+
827
+ ---
828
+
829
+ ### Models
830
+
831
+ #### `_Migration` (internal)
832
+
833
+ The internal system table used by `initialize()` to track applied migrations. It is automatically added to every `DbContextDef` by `defineDbContext` as `_migration`.
834
+
835
+ ```typescript
836
+ // Accessible via the db instance (read-only, internal use)
837
+ const applied = await db._migration().result();
838
+ // Returns: { code: string }[]
839
+ ```
840
+
841
+ ---
842
+
843
+ ### Query Builder (SQL Generation)
844
+
845
+ #### `createQueryBuilder(dialect)`
846
+
847
+ Creates a dialect-specific `QueryBuilderBase` for converting `QueryDef` JSON AST to SQL strings.
848
+
849
+ ```typescript
850
+ import { createQueryBuilder } from "@simplysm/orm-common";
851
+
852
+ const builder = createQueryBuilder("mysql"); // "mysql" | "mssql" | "postgresql"
853
+ const { sql, resultSetIndex, resultSetStride } = builder.build(queryDef);
854
+ ```
855
+
856
+ The `build(def)` method accepts any `QueryDef` and returns `QueryBuildResult`:
857
+
858
+ | Property | Type | Description |
859
+ |----------|------|-------------|
860
+ | `sql` | `string` | Generated SQL string |
861
+ | `resultSetIndex` | `number?` | Which result set to extract (0-based) |
862
+ | `resultSetStride` | `number?` | For multi-INSERT: extract every N-th result set |
863
+
864
+ Available builders (exposed for extension):
865
+
866
+ | Class | Description |
867
+ |-------|-------------|
868
+ | `QueryBuilderBase` | Abstract base class |
869
+ | `ExprRendererBase` | Abstract expression renderer base |
870
+ | `MysqlQueryBuilder` | MySQL implementation |
871
+ | `MysqlExprRenderer` | MySQL expression renderer |
872
+ | `MssqlQueryBuilder` | MSSQL implementation |
873
+ | `MssqlExprRenderer` | MSSQL expression renderer |
874
+ | `PostgresqlQueryBuilder` | PostgreSQL implementation |
875
+ | `PostgresqlExprRenderer` | PostgreSQL expression renderer |
876
+
877
+ ---
226
878
 
227
- ### `queryable(db, tableOrView, as?)`
879
+ ### Low-Level Utilities
228
880
 
229
- Factory function that returns a `() => Queryable` accessor. Used internally by `createDbContext` to attach table/view accessors, but can also be used directly when building custom db context wrappers.
881
+ #### `queryable(db, tableOrView, as?)`
882
+
883
+ Factory that returns a `() => Queryable` accessor. Used internally by `createDbContext`. Available for building custom db context wrappers.
230
884
 
231
885
  ```typescript
232
886
  import { queryable } from "@simplysm/orm-common";
@@ -235,9 +889,9 @@ const getUserQueryable = queryable(db, User);
235
889
  const users = await getUserQueryable().where((u) => [expr.eq(u.isActive, true)]).result();
236
890
  ```
237
891
 
238
- ### `executable(db, procedureBuilder)`
892
+ #### `executable(db, procedureBuilder)`
239
893
 
240
- Factory function that returns a `() => Executable` accessor. Used internally by `createDbContext`.
894
+ Factory that returns a `() => Executable` accessor. Used internally by `createDbContext`.
241
895
 
242
896
  ```typescript
243
897
  import { executable } from "@simplysm/orm-common";
@@ -246,9 +900,9 @@ const getUser = executable(db, GetUserById);
246
900
  const result = await getUser().execute({ userId: 1 });
247
901
  ```
248
902
 
249
- ### `parseQueryResult(rawResults, meta)`
903
+ #### `parseQueryResult(rawResults, meta)`
250
904
 
251
- Parses raw DB query results into typed TypeScript objects. Handles type coercion and JOIN result grouping/nesting.
905
+ Parses raw DB query results into typed TypeScript objects. Handles type coercion (e.g., `"1"` → `number`) and JOIN result grouping/nesting. Returns `undefined` for empty or all-null results.
252
906
 
253
907
  ```typescript
254
908
  import { parseQueryResult } from "@simplysm/orm-common";
@@ -258,19 +912,36 @@ const meta = {
258
912
  joins: {},
259
913
  };
260
914
  const result = await parseQueryResult(rawResults, meta);
915
+ // Returns TRecord[] | undefined
261
916
  ```
262
917
 
263
- ### `parseSearchQuery(searchText)`
918
+ #### `parseSearchQuery(searchText)`
264
919
 
265
920
  Parses a search query string into SQL LIKE patterns for use with `.search()`.
266
921
 
267
922
  ```typescript
268
923
  import { parseSearchQuery } from "@simplysm/orm-common";
269
924
 
270
- const parsed = parseSearchQuery('apple "exact phrase" +required -excluded');
271
- // { or: ["%apple%"], must: ["%exact phrase%", "%required%"], not: ["%excluded%"] }
925
+ const parsed = parseSearchQuery('apple "exact phrase" +required -excluded app*');
926
+ // {
927
+ // or: ["%apple%", "app%"],
928
+ // must: ["%exact phrase%", "%required%"],
929
+ // not: ["%excluded%"]
930
+ // }
272
931
  ```
273
932
 
933
+ Search syntax:
934
+
935
+ | Syntax | Meaning |
936
+ |--------|---------|
937
+ | `term` | OR: contains term |
938
+ | `+term` | MUST: must contain term (AND) |
939
+ | `-term` | NOT: must not contain term |
940
+ | `"exact phrase"` | MUST: exact phrase match |
941
+ | `term*` | Starts with (wildcard) |
942
+ | `*term` | Ends with |
943
+ | `\*`, `\+`, `\-`, `\%`, `\"`, `\\` | Escape special characters |
944
+
274
945
  Returns `ParsedSearchQuery`:
275
946
 
276
947
  | Property | Type | Description |
@@ -279,33 +950,24 @@ Returns `ParsedSearchQuery`:
279
950
  | `must` | `string[]` | Required AND conditions (LIKE patterns) |
280
951
  | `not` | `string[]` | NOT conditions (LIKE patterns) |
281
952
 
282
- ### `createQueryBuilder(dialect)`
953
+ #### `getMatchedPrimaryKeys(fkCols, targetTable)`
283
954
 
284
- Creates a dialect-specific `QueryBuilderBase` instance for converting `QueryDef` JSON AST to SQL strings.
955
+ Matches FK column array against the target table's primary key and returns the PK column name array. Used internally by `include()` and custom JOIN logic.
285
956
 
286
957
  ```typescript
287
- import { createQueryBuilder } from "@simplysm/orm-common";
958
+ import { getMatchedPrimaryKeys } from "@simplysm/orm-common";
288
959
 
289
- const builder = createQueryBuilder("mysql"); // "mysql" | "mssql" | "postgresql"
290
- const { sql } = builder.build(queryDef);
960
+ const pkCols = getMatchedPrimaryKeys(["userId"], User);
961
+ // Returns: ["id"]
291
962
  ```
292
963
 
293
- ### `createColumnFactory()`
294
-
295
- Creates a column type factory used in `TableBuilder.columns()` and `ProcedureBuilder.params()/returns()`. Can also be used standalone for DDL migrations.
296
-
297
- ```typescript
298
- import { createColumnFactory } from "@simplysm/orm-common";
299
-
300
- const c = createColumnFactory();
301
- await db.addColumn({ database: "mydb", name: "User" }, "status", c.varchar(20).nullable());
302
- ```
964
+ ---
303
965
 
304
966
  ## Expression Types
305
967
 
306
968
  ### `ExprUnit<TPrimitive>`
307
969
 
308
- Type-safe expression wrapper. All `expr.*` methods return `ExprUnit`. The generic parameter tracks the TypeScript return type of the expression.
970
+ Type-safe expression wrapper. All `expr.*` SELECT/value methods return `ExprUnit`.
309
971
 
310
972
  ```typescript
311
973
  import { ExprUnit } from "@simplysm/orm-common";
@@ -321,6 +983,10 @@ import { ExprUnit } from "@simplysm/orm-common";
321
983
 
322
984
  Wrapper for WHERE condition expressions. All comparison and logical `expr.*` methods return `WhereExprUnit`.
323
985
 
986
+ ```typescript
987
+ import { WhereExprUnit } from "@simplysm/orm-common";
988
+ ```
989
+
324
990
  ### `ExprInput<TPrimitive>`
325
991
 
326
992
  Union type that accepts either an `ExprUnit<TPrimitive>` or a plain literal value. Most `expr.*` parameters accept `ExprInput` so you can pass raw values without wrapping in `expr.val()`.
@@ -329,22 +995,51 @@ Union type that accepts either an `ExprUnit<TPrimitive>` or a plain literal valu
329
995
  type ExprInput<TPrimitive> = ExprUnit<TPrimitive> | TPrimitive;
330
996
  ```
331
997
 
332
- ### `QueryableRecord<TData>`
333
-
334
- Maps a data record type to its expression counterpart. Each primitive field becomes `ExprUnit`, each array field becomes `QueryableRecord[]`, and each nested object becomes `QueryableRecord`.
335
-
336
998
  ### `SwitchExprBuilder<TPrimitive>`
337
999
 
338
- Builder interface returned by `expr.switch()`:
1000
+ Builder returned by `expr.switch()`:
339
1001
 
340
1002
  | Method | Description |
341
1003
  |--------|-------------|
342
1004
  | `.case(condition, then)` | Add WHEN ... THEN branch |
343
1005
  | `.default(value)` | Add ELSE and finalize to `ExprUnit` |
344
1006
 
1007
+ ### `QueryableRecord<TData>`
1008
+
1009
+ Maps a data record type to its expression counterpart. Each primitive field becomes `ExprUnit`, array relations become `QueryableRecord[]`, and nested objects become `QueryableRecord`.
1010
+
1011
+ ### `NullableQueryableRecord<TData>`
1012
+
1013
+ Like `QueryableRecord` but all primitive fields have `| undefined` added (representing LEFT JOIN NULL propagation).
1014
+
1015
+ ### `QueryableWriteRecord<TData>`
1016
+
1017
+ Write-side record type for `update()`/`upsert()` callbacks. Each field accepts `ExprInput<T>` (either `ExprUnit<T>` or a plain value).
1018
+
1019
+ ### `UnwrapQueryableRecord<R>`
1020
+
1021
+ Reverse mapping from a `QueryableRecord` shape back to a plain `DataRecord`. Used to infer the result type of `select()`.
1022
+
1023
+ ### `PathProxy<TObject>`
1024
+
1025
+ Type-safe path proxy used by `.include()`. Only non-primitive (relation) fields are accessible.
1026
+
1027
+ ```typescript
1028
+ // TypeScript will error if you try to access a primitive column
1029
+ db.post().include((p) => p.author) // OK
1030
+ db.post().include((p) => p.author.company) // OK (chained)
1031
+ // db.post().include((p) => p.title) // Error: title is ColumnPrimitive
1032
+ ```
1033
+
345
1034
  ### `toExpr(value)`
346
1035
 
347
- Converts an `ExprInput` to a raw `Expr` JSON AST. Used internally; exposed for custom QueryBuilder extensions.
1036
+ Converts an `ExprInput` to a raw `Expr` JSON AST. Exposed for custom `QueryBuilder` extensions.
1037
+
1038
+ ```typescript
1039
+ import { toExpr } from "@simplysm/orm-common";
1040
+ ```
1041
+
1042
+ ---
348
1043
 
349
1044
  ## Type Reference
350
1045
 
@@ -398,28 +1093,15 @@ Converts an `ExprInput` to a raw `Expr` JSON AST. Used internally; exposed for c
398
1093
  | `InferUpdateColumns<T>` | UPDATE type (all optional) |
399
1094
  | `InferColumnExprs<T>` | Expression input type from a `ColumnBuilderRecord` |
400
1095
  | `InferDeepRelations<T>` | Infer relation types (all optional) |
401
- | `QueryableWriteRecord<TData>` | Write-side record type for update/upsert callbacks (accepts `ExprInput<T>` -- both `ExprUnit<T>` and plain values) |
1096
+ | `QueryableWriteRecord<TData>` | Write-side record type for update/upsert callbacks (accepts `ExprInput<T>`) |
402
1097
  | `PathProxy<TObject>` | Type-safe path proxy for `.include()` navigation |
1098
+ | `DataToColumnBuilderRecord<TData>` | Convert `DataRecord` to `ColumnBuilderRecord` (for `insertInto` type checking) |
1099
+ | `RequiredInsertKeys<T>` | Keys that are required in INSERT (no autoIncrement, nullable, or default) |
1100
+ | `OptionalInsertKeys<T>` | Keys that are optional in INSERT |
1101
+ | `ExtractRelationTarget<T>` | Extract the TypeScript type of an FK/RelationKey target |
1102
+ | `ExtractRelationTargetResult<T>` | Extract the TypeScript type of an FKTarget/RelationKeyTarget (array or single) |
403
1103
 
404
- ## Security Notes
405
-
406
- orm-common uses **enhanced string escaping** instead of parameter binding due to its dynamic query nature.
407
- Always perform input validation at the application level.
408
-
409
- ```typescript
410
- // Bad: Direct user input
411
- const userInput = req.query.name; // e.g. malicious SQL payload
412
- await db.user().where((u) => [expr.eq(u.name, userInput)]).result();
413
-
414
- // Good: Validate before use
415
- const userName = validateUserName(req.query.name);
416
- await db.user().where((u) => [expr.eq(u.name, userName)]).result();
417
-
418
- // Better: Type coercion
419
- const userId = Number(req.query.id);
420
- if (Number.isNaN(userId)) throw new Error("Invalid ID");
421
- await db.user().where((u) => [expr.eq(u.id, userId)]).result();
422
- ```
1104
+ ---
423
1105
 
424
1106
  ## Quick Start
425
1107
 
@@ -464,6 +1146,26 @@ await db.connect(async () => {
464
1146
  });
465
1147
  ```
466
1148
 
1149
+ ## Security Notes
1150
+
1151
+ orm-common uses **enhanced string escaping** instead of parameter binding due to its dynamic query nature.
1152
+ Always perform input validation at the application level.
1153
+
1154
+ ```typescript
1155
+ // Bad: Direct user input
1156
+ const userInput = req.query.name; // e.g. malicious SQL payload
1157
+ await db.user().where((u) => [expr.eq(u.name, userInput)]).result();
1158
+
1159
+ // Good: Validate before use
1160
+ const userName = validateUserName(req.query.name);
1161
+ await db.user().where((u) => [expr.eq(u.name, userName)]).result();
1162
+
1163
+ // Better: Type coercion
1164
+ const userId = Number(req.query.id);
1165
+ if (Number.isNaN(userId)) throw new Error("Invalid ID");
1166
+ await db.user().where((u) => [expr.eq(u.id, userId)]).result();
1167
+ ```
1168
+
467
1169
  ## License
468
1170
 
469
1171
  Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/orm-common",
3
- "version": "13.0.42",
3
+ "version": "13.0.43",
4
4
  "description": "심플리즘 패키지 - ORM 모듈 (common)",
5
5
  "author": "김석래",
6
6
  "license": "Apache-2.0",
@@ -19,6 +19,6 @@
19
19
  ],
20
20
  "sideEffects": false,
21
21
  "dependencies": {
22
- "@simplysm/core-common": "13.0.42"
22
+ "@simplysm/core-common": "13.0.43"
23
23
  }
24
24
  }