@simplysm/orm-common 14.0.46 → 14.0.48

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 (54) hide show
  1. package/README.md +269 -0
  2. package/dist/exec/queryable.d.ts +7 -2
  3. package/dist/exec/queryable.d.ts.map +1 -1
  4. package/dist/exec/queryable.js +10 -2
  5. package/dist/exec/queryable.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/query-builder/base/query-builder-base.d.ts +7 -0
  11. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  12. package/dist/query-builder/base/query-builder-base.js +9 -0
  13. package/dist/query-builder/base/query-builder-base.js.map +1 -1
  14. package/dist/query-builder/mssql/mssql-expr-renderer.js +1 -1
  15. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  16. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  17. package/dist/query-builder/mssql/mssql-query-builder.js +3 -0
  18. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
  19. package/dist/query-builder/mysql/mysql-expr-renderer.js +1 -1
  20. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
  21. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  22. package/dist/query-builder/mysql/mysql-query-builder.js +23 -7
  23. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  24. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +1 -1
  25. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
  26. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  27. package/dist/query-builder/postgresql/postgresql-query-builder.js +3 -0
  28. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  29. package/dist/utils/pick-result-sets.d.ts +11 -0
  30. package/dist/utils/pick-result-sets.d.ts.map +1 -0
  31. package/dist/utils/pick-result-sets.js +23 -0
  32. package/dist/utils/pick-result-sets.js.map +1 -0
  33. package/dist/utils/result-parser.d.ts.map +1 -1
  34. package/dist/utils/result-parser.js +36 -7
  35. package/dist/utils/result-parser.js.map +1 -1
  36. package/docs/core.md +188 -0
  37. package/docs/expression.md +190 -0
  38. package/docs/models.md +17 -0
  39. package/docs/query-builder.md +97 -0
  40. package/docs/queryable-executable.md +311 -0
  41. package/docs/schema-builders.md +364 -0
  42. package/docs/types.md +537 -0
  43. package/package.json +4 -3
  44. package/src/exec/queryable.ts +13 -2
  45. package/src/index.ts +1 -0
  46. package/src/query-builder/base/query-builder-base.ts +16 -6
  47. package/src/query-builder/mssql/mssql-expr-renderer.ts +3 -3
  48. package/src/query-builder/mssql/mssql-query-builder.ts +10 -7
  49. package/src/query-builder/mysql/mysql-expr-renderer.ts +3 -3
  50. package/src/query-builder/mysql/mysql-query-builder.ts +40 -22
  51. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +3 -3
  52. package/src/query-builder/postgresql/postgresql-query-builder.ts +10 -7
  53. package/src/utils/pick-result-sets.ts +30 -0
  54. package/src/utils/result-parser.ts +56 -22
@@ -0,0 +1,190 @@
1
+ # Expression
2
+
3
+ ## `expr`
4
+
5
+ Dialect 독립적 SQL expression builder. SQL 문자열 대신 JSON AST(Expr)를 생성하며, QueryBuilder가 각 DBMS(MySQL, MSSQL, PostgreSQL)로 변환한다.
6
+
7
+ ```typescript
8
+ export const expr: {
9
+ // 값 생성
10
+ val<TStr extends ColumnPrimitiveStr>(dataType: TStr, value: ColumnPrimitiveMap[TStr] | undefined): ExprUnit;
11
+ col<TStr extends ColumnPrimitiveStr>(dataType: ColumnPrimitiveStr, ...path: string[]): ExprUnit;
12
+ raw<T extends ColumnPrimitiveStr>(dataType: T): (strings: TemplateStringsArray, ...values: ExprInput[]) => ExprUnit;
13
+
14
+ // WHERE - 비교 연산자
15
+ eq<T>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit;
16
+ gt<T>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit;
17
+ lt<T>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit;
18
+ gte<T>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit;
19
+ lte<T>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit;
20
+ between<T>(source: ExprUnit<T>, from?: ExprInput<T>, to?: ExprInput<T>): WhereExprUnit;
21
+
22
+ // WHERE - NULL check
23
+ null<T>(source: ExprUnit<T>): WhereExprUnit;
24
+
25
+ // WHERE - 문자열 검색
26
+ like(source: ExprUnit<string | undefined>, pattern: ExprInput<string | undefined>): WhereExprUnit;
27
+ regexp(source: ExprUnit<string | undefined>, pattern: ExprInput<string | undefined>): WhereExprUnit;
28
+
29
+ // WHERE - IN / EXISTS
30
+ in<T>(source: ExprUnit<T>, values: ExprInput<T>[]): WhereExprUnit;
31
+ inQuery<T>(source: ExprUnit<T>, query: Queryable): WhereExprUnit;
32
+ exists(query: Queryable): WhereExprUnit;
33
+
34
+ // WHERE - 논리 연산자
35
+ not(arg: WhereExprUnit): WhereExprUnit;
36
+ and(conditions: WhereExprUnit[]): WhereExprUnit;
37
+ or(conditions: WhereExprUnit[]): WhereExprUnit;
38
+
39
+ // SELECT - 문자열
40
+ concat(...args: ExprInput<string | undefined>[]): ExprUnit<string>;
41
+ left<T>(source: ExprUnit<T>, length: ExprInput<number>): ExprUnit<T>;
42
+ right<T>(source: ExprUnit<T>, length: ExprInput<number>): ExprUnit<T>;
43
+ trim<T>(source: ExprUnit<T>): ExprUnit<T>;
44
+ padStart<T>(source: ExprUnit<T>, length: ExprInput<number>, fillString: ExprInput<string>): ExprUnit<T>;
45
+ replace<T>(source: ExprUnit<T>, from: ExprInput<string>, to: ExprInput<string>): ExprUnit<T>;
46
+ upper<T>(source: ExprUnit<T>): ExprUnit<T>;
47
+ lower<T>(source: ExprUnit<T>): ExprUnit<T>;
48
+ length(source: ExprUnit<string | undefined>): ExprUnit<number>;
49
+ byteLength(source: ExprUnit<string | undefined>): ExprUnit<number>;
50
+ substring<T>(source: ExprUnit<T>, start: ExprInput<number>, length?: ExprInput<number>): ExprUnit<T>;
51
+ indexOf(source: ExprUnit<string | undefined>, search: ExprInput<string>): ExprUnit<number>;
52
+
53
+ // SELECT - 숫자
54
+ abs<T>(source: ExprUnit<T>): ExprUnit<T>;
55
+ round<T>(source: ExprUnit<T>, digits: number): ExprUnit<T>;
56
+ ceil<T>(source: ExprUnit<T>): ExprUnit<T>;
57
+ floor<T>(source: ExprUnit<T>): ExprUnit<T>;
58
+
59
+ // SELECT - 날짜/시간
60
+ year(source: ExprUnit): ExprUnit<number>;
61
+ month(source: ExprUnit): ExprUnit<number>;
62
+ day(source: ExprUnit): ExprUnit<number>;
63
+ hour(source: ExprUnit): ExprUnit<number>;
64
+ minute(source: ExprUnit): ExprUnit<number>;
65
+ second(source: ExprUnit): ExprUnit<number>;
66
+ isoWeek(source: ExprUnit<DateOnly | undefined>): ExprUnit<number>;
67
+ isoWeekStartDate<T>(source: ExprUnit<T>): ExprUnit<T>;
68
+ isoYearMonth<T>(source: ExprUnit<T>): ExprUnit<T>;
69
+ dateDiff<T>(unit: DateUnit, from: ExprInput<T>, to: ExprInput<T>): ExprUnit<number>;
70
+ dateAdd<T>(unit: DateUnit, source: ExprUnit<T>, value: ExprInput<number>): ExprUnit<T>;
71
+ formatDate<T>(source: ExprUnit<T>, format: string): ExprUnit<string>;
72
+
73
+ // SELECT - 조건
74
+ coalesce(...args: ExprInput[]): ExprUnit;
75
+ nullIf<T>(source: ExprUnit<T>, value: ExprInput<T>): ExprUnit<T | undefined>;
76
+ is(condition: WhereExprUnit): ExprUnit<boolean>;
77
+ switch<T>(): SwitchExprBuilder<T>;
78
+ if<T>(condition: WhereExprUnit, then: ExprInput<T>, else_: ExprInput<T>): ExprUnit<T>;
79
+
80
+ // SELECT - 집계
81
+ count(arg?: ExprUnit, distinct?: boolean): ExprUnit<number>;
82
+ sum(arg: ExprUnit<number | undefined>): ExprUnit<number | undefined>;
83
+ avg(arg: ExprUnit<number | undefined>): ExprUnit<number | undefined>;
84
+ max<T>(arg: ExprUnit<T>): ExprUnit<T | undefined>;
85
+ min<T>(arg: ExprUnit<T>): ExprUnit<T | undefined>;
86
+
87
+ // SELECT - 기타
88
+ greatest<T>(...args: ExprInput<T>[]): ExprUnit<T>;
89
+ least<T>(...args: ExprInput<T>[]): ExprUnit<T>;
90
+ rowNum(): ExprUnit<number>;
91
+ random(): ExprUnit<number>;
92
+ cast<T, TDataType>(source: ExprUnit<T>, targetType: TDataType): ExprUnit;
93
+ subquery<TStr>(dataType: TStr, queryable: { getSelectQueryDef(): SelectQueryDef }): ExprUnit;
94
+
95
+ // SELECT - Window 함수
96
+ rowNumber(spec: WinSpecInput): ExprUnit<number>;
97
+ rank(spec: WinSpecInput): ExprUnit<number>;
98
+ denseRank(spec: WinSpecInput): ExprUnit<number>;
99
+ ntile(n: number, spec: WinSpecInput): ExprUnit<number>;
100
+ lag<T>(column: ExprUnit<T>, spec: WinSpecInput, options?: { offset?: number; default?: ExprInput<T> }): ExprUnit<T | undefined>;
101
+ lead<T>(column: ExprUnit<T>, spec: WinSpecInput, options?: { offset?: number; default?: ExprInput<T> }): ExprUnit<T | undefined>;
102
+ firstValue<T>(column: ExprUnit<T>, spec: WinSpecInput): ExprUnit<T | undefined>;
103
+ lastValue<T>(column: ExprUnit<T>, spec: WinSpecInput): ExprUnit<T | undefined>;
104
+ sumOver(column: ExprUnit<number | undefined>, spec: WinSpecInput): ExprUnit<number | undefined>;
105
+ avgOver(column: ExprUnit<number | undefined>, spec: WinSpecInput): ExprUnit<number | undefined>;
106
+ countOver(spec: WinSpecInput, column?: ExprUnit): ExprUnit<number>;
107
+ minOver<T>(column: ExprUnit<T>, spec: WinSpecInput): ExprUnit<T | undefined>;
108
+ maxOver<T>(column: ExprUnit<T>, spec: WinSpecInput): ExprUnit<T | undefined>;
109
+
110
+ // Helper
111
+ toExpr(value: ExprInput<ColumnPrimitive>): Expr;
112
+ };
113
+ ```
114
+
115
+ ### 주요 함수 설명
116
+
117
+ - **val()**: 리터럴 값을 ExprUnit으로 래핑. dataType에 맞는 기본 타입으로 확장.
118
+ - **raw()**: 태그드 템플릿 리터럴로 Raw SQL expression 생성. 보간된 값은 자동 파라미터화.
119
+ - **eq()**: NULL 안전 동등 비교. MySQL: `<=>`, MSSQL/PostgreSQL: `IS NULL OR =`.
120
+ - **between()**: from/to가 undefined이면 해당 방향 제한 없음.
121
+ - **inQuery()**: 서브쿼리는 단일 column만 SELECT해야 함.
122
+ - **coalesce()**: 마지막 인수가 non-nullable이면 결과도 non-nullable.
123
+ - **switch()**: 메서드 체이닝으로 CASE WHEN 구성. `.case().case().default()`.
124
+ - **subquery()**: 스칼라 서브쿼리. 정확히 하나의 행과 하나의 column 반환해야 함.
125
+ - **rowNumber()/rank()/denseRank()**: Window 함수. partitionBy와 orderBy로 구간 지정.
126
+ - **lag()/lead()**: 이전/다음 행 값 참조. offset과 default 지정 가능.
127
+ - **sumOver()/avgOver()/countOver()/minOver()/maxOver()**: Window 집계 함수.
128
+
129
+ ## `SwitchExprBuilder`
130
+
131
+ CASE WHEN 표현식 빌더 인터페이스.
132
+
133
+ ```typescript
134
+ export interface SwitchExprBuilder<TPrimitive extends ColumnPrimitive> {
135
+ case(condition: WhereExprUnit, then: ExprInput<TPrimitive>): SwitchExprBuilder<TPrimitive>;
136
+ default(value: ExprInput<TPrimitive>): ExprUnit<TPrimitive>;
137
+ }
138
+ ```
139
+
140
+ ## `ExprUnit`
141
+
142
+ 타입 안전 표현식 래퍼. TypeScript 제네릭을 사용하여 표현식 반환 타입을 추적한다.
143
+
144
+ ```typescript
145
+ export class ExprUnit<TPrimitive extends ColumnPrimitive> {
146
+ readonly $infer!: TPrimitive;
147
+ readonly dataType: ColumnPrimitiveStr;
148
+ readonly expr: Expr;
149
+
150
+ get n(): ExprUnit<NonNullable<TPrimitive>>;
151
+
152
+ constructor(dataType: ColumnPrimitiveStr, expr: Expr);
153
+ }
154
+ ```
155
+
156
+ | Field | Type | Description |
157
+ |-------|------|-------------|
158
+ | `$infer` | `TPrimitive` | 타입 추론용 phantom 필드 |
159
+ | `dataType` | `ColumnPrimitiveStr` | 데이터 타입 문자열 |
160
+ | `expr` | `Expr` | 내부 AST 표현 |
161
+ | `n` (getter) | `ExprUnit<NonNullable<TPrimitive>>` | non-nullable 버전 반환 |
162
+
163
+ ## `WhereExprUnit`
164
+
165
+ WHERE 절용 표현식 래퍼.
166
+
167
+ ```typescript
168
+ export class WhereExprUnit {
169
+ readonly expr: WhereExpr;
170
+ constructor(expr: WhereExpr);
171
+ }
172
+ ```
173
+
174
+ ## `ExprInput`
175
+
176
+ ExprUnit 또는 리터럴 값을 받는 입력 타입.
177
+
178
+ ```typescript
179
+ export type ExprInput<TPrimitive extends ColumnPrimitive> = ExprUnit<TPrimitive> | TPrimitive;
180
+ ```
181
+
182
+ ## `toExpr`
183
+
184
+ ExprInput을 Expr AST로 변환하는 독립 함수. 커스텀 표현식 빌더를 작성할 때 리터럴 값과 ExprUnit을 통일된 Expr로 변환할 때 사용한다.
185
+
186
+ ```typescript
187
+ export function toExpr(value: ExprInput<ColumnPrimitive>): Expr
188
+ ```
189
+
190
+ `ExprUnit`이면 `.expr`을 반환하고, 리터럴 값이면 `{ type: "value", value }`로 래핑한다.
package/docs/models.md ADDED
@@ -0,0 +1,17 @@
1
+ # Models
2
+
3
+ ## `_Migration`
4
+
5
+ 시스템 마이그레이션 테이블 정의. DbContext에서 `initialize()` 호출 시 자동으로 사용되는 내부 테이블이다.
6
+
7
+ ```typescript
8
+ export const _Migration: TableBuilder<{ code: ColumnBuilder<string, { type: "string"; dataType: { type: "varchar"; length: 255 } }> }, never>;
9
+ ```
10
+
11
+ 테이블 구조:
12
+
13
+ | Column | Type | Description |
14
+ |--------|------|-------------|
15
+ | `code` | `varchar(255)` | 마이그레이션 코드 (PK) |
16
+
17
+ `_migration` 테이블은 적용된 마이그레이션 코드를 저장하여 중복 실행을 방지한다. `DbContext.initialize()` 호출 시 이 테이블을 조회하여 미적용 마이그레이션만 실행하고, 마이그레이션이 실제로 적용되었으면 `true`를 반환한다.
@@ -0,0 +1,97 @@
1
+ # Query Builder
2
+
3
+ ## `createQueryBuilder`
4
+
5
+ 주어진 Dialect에 맞는 QueryBuilder 인스턴스를 생성한다.
6
+
7
+ ```typescript
8
+ export function createQueryBuilder(dialect: Dialect): QueryBuilderBase;
9
+ ```
10
+
11
+ | Dialect | Builder |
12
+ |---------|---------|
13
+ | `"mysql"` | `MysqlQueryBuilder` |
14
+ | `"mssql"` | `MssqlQueryBuilder` |
15
+ | `"postgresql"` | `PostgresqlQueryBuilder` |
16
+
17
+ ## `QueryBuilderBase`
18
+
19
+ QueryDef -> SQL 문자열 변환 추상 클래스. 각 Dialect별 구현체가 상속한다.
20
+
21
+ ```typescript
22
+ export abstract class QueryBuilderBase {
23
+ build(queryDef: QueryDef): QueryBuildResult;
24
+ }
25
+ ```
26
+
27
+ `build()` 메서드는 `QueryDef` AST를 받아 SQL 문자열과 결과 셋 메타데이터를 포함하는 `QueryBuildResult`를 반환한다.
28
+
29
+ ## `ExprRendererBase`
30
+
31
+ Expr -> SQL 표현식 문자열 변환 추상 클래스. 각 Dialect별 구현체가 상속한다.
32
+
33
+ ```typescript
34
+ export abstract class ExprRendererBase {
35
+ // 내부적으로 QueryBuilderBase에서 사용
36
+ }
37
+ ```
38
+
39
+ ## `MysqlQueryBuilder`
40
+
41
+ MySQL용 QueryBuilder 구현체.
42
+
43
+ ```typescript
44
+ export class MysqlQueryBuilder extends QueryBuilderBase {
45
+ // MySQL 8.0.14+ 문법 사용
46
+ }
47
+ ```
48
+
49
+ ## `MysqlExprRenderer`
50
+
51
+ MySQL용 ExprRenderer 구현체.
52
+
53
+ ```typescript
54
+ export class MysqlExprRenderer extends ExprRendererBase {
55
+ // MySQL 고유 함수/구문 사용 (<=>, IFNULL, DATE_FORMAT 등)
56
+ }
57
+ ```
58
+
59
+ ## `MssqlQueryBuilder`
60
+
61
+ MSSQL용 QueryBuilder 구현체.
62
+
63
+ ```typescript
64
+ export class MssqlQueryBuilder extends QueryBuilderBase {
65
+ // MSSQL 2012+ 문법 사용 (OFFSET/FETCH, OUTPUT 등)
66
+ }
67
+ ```
68
+
69
+ ## `MssqlExprRenderer`
70
+
71
+ MSSQL용 ExprRenderer 구현체.
72
+
73
+ ```typescript
74
+ export class MssqlExprRenderer extends ExprRendererBase {
75
+ // MSSQL 고유 함수/구문 사용 (ISNULL, CONVERT, CHARINDEX 등)
76
+ }
77
+ ```
78
+
79
+ ## `PostgresqlQueryBuilder`
80
+
81
+ PostgreSQL용 QueryBuilder 구현체.
82
+
83
+ ```typescript
84
+ export class PostgresqlQueryBuilder extends QueryBuilderBase {
85
+ // PostgreSQL 9.0+ 문법 사용
86
+ }
87
+ ```
88
+
89
+ ## `PostgresqlExprRenderer`
90
+
91
+ PostgreSQL용 ExprRenderer 구현체.
92
+
93
+ ```typescript
94
+ export class PostgresqlExprRenderer extends ExprRendererBase {
95
+ // PostgreSQL 고유 함수/구문 사용 (COALESCE, TO_CHAR, POSITION 등)
96
+ }
97
+ ```
@@ -0,0 +1,311 @@
1
+ # Queryable / Executable
2
+
3
+ ## `Queryable`
4
+
5
+ 체이닝 방식으로 table/view에 대한 SELECT, INSERT, UPDATE, DELETE, UPSERT 쿼리를 구성하는 클래스.
6
+
7
+ ```typescript
8
+ export class Queryable<TData extends DataRecord, TFrom extends TableBuilder | never> {
9
+ constructor(readonly meta: QueryableMeta<TData>);
10
+
11
+ // SELECT / DISTINCT / LOCK
12
+ select<R>(fn: (columns: QueryableRecord<TData>) => R): Queryable<UnwrapQueryableRecord<R>, never>;
13
+ distinct(): Queryable<TData, never>;
14
+ lock(): Queryable<TData, TFrom>;
15
+
16
+ // TOP / LIMIT
17
+ top(count: number): Queryable<TData, TFrom>;
18
+ limit(skip: number, take: number): Queryable<TData, TFrom>;
19
+
20
+ // ORDER BY (lambda 또는 체인 경로 문자열)
21
+ orderBy(fnOrKey: string | ((columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>), orderBy?: "ASC" | "DESC"): Queryable<TData, TFrom>;
22
+
23
+ // WHERE
24
+ where(predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[]): Queryable<TData, TFrom>;
25
+ search(fn: (columns: QueryableRecord<TData>) => ExprUnit<string | undefined>[], searchText: string): Queryable<TData, TFrom>;
26
+
27
+ // GROUP BY / HAVING
28
+ groupBy(fn: (columns: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>[]): Queryable<TData, never>;
29
+ having(predicate: (columns: QueryableRecord<TData>) => WhereExprUnit[]): Queryable<TData, never>;
30
+
31
+ // JOIN
32
+ join<A extends string, R>(as: A, fn: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>): Queryable<TData & { [K in A]?: R[] }, TFrom>;
33
+ joinSingle<A extends string, R>(as: A, fn: (qr: JoinQueryable, cols: QueryableRecord<TData>) => Queryable<R, any>): Queryable<TData & { [K in A]?: R }, TFrom>;
34
+
35
+ // INCLUDE (관계 기반 자동 JOIN)
36
+ include(fn: (item: PathProxy<TData>) => PathProxy<any>): Queryable<TData, TFrom>;
37
+
38
+ // WRAP / UNION
39
+ wrap(): Queryable<TData, never>;
40
+ static union<TData>(...queries: Queryable<TData, any>[]): Queryable<TData, never>;
41
+
42
+ // WITH RECURSIVE
43
+ recursive(fn: (qr: RecursiveQueryable<TData>) => Queryable<TData, any>): Queryable<TData, never>;
44
+
45
+ // SELECT 실행
46
+ execute(): Promise<TData[]>;
47
+ single(): Promise<TData | undefined>;
48
+ first(): Promise<TData | undefined>;
49
+ count(fn?: (cols: QueryableRecord<TData>) => ExprUnit<ColumnPrimitive>): Promise<number>;
50
+ exists(): Promise<boolean>;
51
+
52
+ // INSERT
53
+ insert(records: TFrom["$inferInsert"][]): Promise<void>;
54
+ insert<K>(records: TFrom["$inferInsert"][], outputColumns: K[]): Promise<Pick<TFrom["$inferColumns"], K>[]>;
55
+ insertIfNotExists(record: TFrom["$inferInsert"]): Promise<void>;
56
+ insertIfNotExists<K>(record: TFrom["$inferInsert"], outputColumns: K[]): Promise<Pick<TFrom["$inferColumns"], K>>;
57
+ insertInto<TTable>(targetTable: TTable): Promise<void>;
58
+ insertInto<TTable, TOut>(targetTable: TTable, outputColumns: TOut[]): Promise<Pick<TData, TOut>[]>;
59
+
60
+ // UPDATE
61
+ update(recordFwd: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>): Promise<void>;
62
+ update<K>(recordFwd: ..., outputColumns: K[]): Promise<Pick<TFrom["$inferColumns"], K>[]>;
63
+
64
+ // DELETE
65
+ delete(): Promise<void>;
66
+ delete<K>(outputColumns: K[]): Promise<Pick<TFrom["$inferColumns"], K>[]>;
67
+
68
+ // UPSERT
69
+ upsert(updateFn: (cols: QueryableRecord<TData>) => QueryableWriteRecord<TFrom["$inferUpdate"]>): Promise<void>;
70
+ upsert<U>(updateFn: ..., insertFn: (updateRecord: U) => QueryableWriteRecord<TFrom["$inferInsert"]>): Promise<void>;
71
+ upsert<U, K>(updateFn: ..., insertFn: ..., outputColumns: K[]): Promise<Pick<TFrom["$inferColumns"], K>[]>;
72
+
73
+ // FK 제약조건
74
+ switchFk(enabled: boolean): Promise<void>;
75
+
76
+ // QueryDef 생성
77
+ getSelectQueryDef(): SelectQueryDef;
78
+ getResultMeta(outputColumns?: string[]): ResultMeta;
79
+ getInsertQueryDef(records: ..., outputColumns?: ...): InsertQueryDef;
80
+ getInsertIfNotExistsQueryDef(record: ..., outputColumns?: ...): InsertIfNotExistsQueryDef;
81
+ getInsertIntoQueryDef(targetTable: ..., outputColumns?: ...): InsertIntoQueryDef;
82
+ getUpdateQueryDef(recordFwd: ..., outputColumns?: ...): UpdateQueryDef;
83
+ getDeleteQueryDef(outputColumns?: ...): DeleteQueryDef;
84
+ getUpsertQueryDef(updateRecordFn: ..., insertRecordFn: ..., outputColumns?: ...): UpsertQueryDef;
85
+ }
86
+ ```
87
+
88
+ ### 주요 메서드 설명
89
+
90
+ - **select()**: SELECT할 column 지정. 프록시 객체를 통해 column을 매핑.
91
+ - **where()**: WHERE 조건 추가. 여러 번 호출 시 AND로 결합.
92
+ - **search()**: 텍스트 검색. `parseSearchQuery`를 내부 사용하여 LIKE 패턴 변환.
93
+ - **join()**: 1:N LEFT OUTER JOIN (결과 배열). **joinSingle()**: N:1/1:1 LEFT OUTER JOIN (단일 객체).
94
+ - **include()**: TableBuilder에 정의된 관계를 기반으로 자동 JOIN. 중첩 관계도 지원 (`p.author.company`).
95
+ - **wrap()**: 현재 Queryable을 서브쿼리로 래핑. distinct()/groupBy() 후 count() 시 필요.
96
+ - **recursive()**: WITH RECURSIVE CTE 생성. 계층 데이터 조회에 사용.
97
+ - **upsert()**: WHERE 조건 일치 시 UPDATE, 미일치 시 INSERT. UPDATE/INSERT 데이터를 각각 지정 가능.
98
+ - **insert()**: MSSQL 1000행 제한을 위해 자동 청크 분할.
99
+ - **count()**: distinct()/groupBy() 이후 직접 호출 불가. wrap() 먼저 사용해야 함.
100
+
101
+ ### include 후 relation 접근: `!` vs `?.`
102
+
103
+ `@simplysm/orm-common`의 relation은 타입 시스템상 **항상 optional**로 추론된다 (`x.rfq?: ...`). 하지만 실제 런타임 nullability는 **관계 종류**와 **FK 컬럼의 nullable 여부**로 결정된다.
104
+
105
+ > `foreignKey`와 `relationKey`(N:1)는 동일 규칙, `foreignKeyTarget`과 `relationKeyTarget`(1:N · 1:1 역참조)도 동일 규칙으로 취급한다.
106
+
107
+ **규칙:**
108
+
109
+ - **N:1 관계(`foreignKey` / `relationKey`)** — 본인이 FK 컬럼을 보유하므로 FK 컬럼의 nullable 여부로 분기
110
+ - **FK NOT NULL + `.include(x => x.relation)`**: **`x.relation!.field`** (non-null assertion)
111
+ - 근거: `.include()`로 JOIN이 강제되고 FK NOT NULL이므로 DB 레벨에서 존재 보장
112
+ - `?.` 사용 금지 — 절대 도달하지 않는 undefined 경로에 방어 코드를 넣는 것은 dead branch 생성
113
+ - **FK nullable + `.include()`**: **`x.relation?.field`** (optional chaining)
114
+ - 근거: FK가 null이면 relation도 없음
115
+ - **역참조(`foreignKeyTarget` / `relationKeyTarget`, `single` 무관) + `.include()`**: **항상 `x.relation?.field`** (optional chaining)
116
+ - 근거: 본인이 key를 가진 게 아니라 상대 테이블이 본인을 참조한다. include 결과는 array(0개 가능) 또는 single:true의 단일 객체(존재하지 않을 수 있음) 모두 항상 nullable이다. NOT NULL 보장 불가
117
+ - `!` 사용 금지
118
+ - **`.include()` 미사용**: relation 참조 금지 (조회되지 않음)
119
+
120
+ **예시:**
121
+
122
+ ```ts
123
+ // N:1 — NewPoItem.rfqId: NOT NULL, Customer.endCustomerId: NOT NULL
124
+ db.newPoItem()
125
+ .include((x) => x.rfq)
126
+ .include((x) => x.customer.endCustomer) // 체이닝도 동일 규칙
127
+ .select((x) => ({
128
+ rfqCode: x.rfq!.code, // NOT NULL → !
129
+ endCustomerCode: x.customer!.endCustomer!.code, // 둘 다 NOT NULL → !
130
+ }));
131
+
132
+ // N:1 — Distributor.defaultCustomerId: nullable
133
+ db.distributor()
134
+ .include((x) => x.defaultCustomer)
135
+ .select((x) => ({
136
+ defaultCustomerName: x.defaultCustomer?.name, // nullable → ?.
137
+ }));
138
+
139
+ // 1:N 역참조 (foreignKeyTarget, array)
140
+ db.user()
141
+ .include((x) => x.posts)
142
+ .select((x) => ({
143
+ postCount: x.posts?.length, // 항상 ?.
144
+ firstTitle: x.posts?.[0]?.title, // 배열 요소 접근도 ?.
145
+ }));
146
+
147
+ // 1:1 역참조 (foreignKeyTarget, single: true)
148
+ db.user()
149
+ .include((x) => x.profile)
150
+ .select((x) => ({
151
+ bio: x.profile?.bio, // single이어도 항상 ?. (Profile 행이 없을 수 있음)
152
+ }));
153
+ ```
154
+
155
+ **잘못된 예시:**
156
+
157
+ - `rfqCode: x.rfq?.code` (N:1, FK NOT NULL인데 `?.` 사용) — 타입 추론 한계를 회피만 하고 의도를 숨김. 린트가 잡지 못함
158
+ - `rfqCode: x.rfq.code` (assertion 없음) — TS 에러 `possibly 'undefined'`
159
+ - `postCount: x.posts!.length` (역참조에 `!` 사용) — 빈 array일 가능성을 무시
160
+ - `bio: x.profile!.bio` (역참조 single에 `!` 사용) — 상대 행이 없으면 undefined
161
+
162
+ ## `queryable`
163
+
164
+ Queryable 생성 팩토리 함수. DbContext 내부에서 사용.
165
+
166
+ ```typescript
167
+ export function queryable<TBuilder extends TableBuilder | ViewBuilder>(
168
+ db: DbContextBase,
169
+ builder: TBuilder,
170
+ alias?: string,
171
+ ): () => Queryable<TBuilder["$inferSelect"], TBuilder extends TableBuilder ? TBuilder : never>;
172
+ ```
173
+
174
+ ## `getMatchedPrimaryKeys`
175
+
176
+ FK column 배열과 대상 테이블의 PK를 매칭하여 PK column 이름 배열을 반환한다.
177
+
178
+ ```typescript
179
+ export function getMatchedPrimaryKeys(
180
+ fkCols: string[],
181
+ targetTable: TableBuilder<any, any>,
182
+ ): string[];
183
+ ```
184
+
185
+ ## `QueryableRecord`
186
+
187
+ Queryable의 column 프록시 레코드 타입. 각 column이 `ExprUnit`으로 래핑되고, 중첩 관계는 재귀적으로 표현된다.
188
+
189
+ ```typescript
190
+ export type QueryableRecord<TData extends DataRecord> = {
191
+ [K in keyof TData]: TData[K] extends ColumnPrimitive
192
+ ? ExprUnit<TData[K]>
193
+ : TData[K] extends (infer U)[]
194
+ ? U extends DataRecord
195
+ ? QueryableRecord<U>[]
196
+ : never
197
+ : TData[K] extends (infer U)[] | undefined
198
+ ? U extends DataRecord
199
+ ? QueryableRecord<U>[] | undefined
200
+ : never
201
+ : TData[K] extends DataRecord
202
+ ? QueryableRecord<TData[K]>
203
+ : TData[K] extends DataRecord | undefined
204
+ ? QueryableRecord<Exclude<TData[K], undefined>> | undefined
205
+ : never;
206
+ };
207
+ ```
208
+
209
+ ## `QueryableWriteRecord`
210
+
211
+ UPDATE/INSERT용 column 레코드 타입. `ExprUnit` 또는 리터럴 값을 받는다.
212
+
213
+ ```typescript
214
+ export type QueryableWriteRecord<TData> = {
215
+ [K in keyof TData]: TData[K] extends ColumnPrimitive ? ExprInput<TData[K]> : never;
216
+ };
217
+ ```
218
+
219
+ ## `UnwrapQueryableRecord`
220
+
221
+ QueryableRecord에서 실제 데이터 타입을 추출한다.
222
+
223
+ ```typescript
224
+ export type UnwrapQueryableRecord<R> = {
225
+ [K in keyof R as K extends symbol ? never : K]: R[K] extends ExprUnit<infer T>
226
+ ? T
227
+ : NonNullable<R[K]> extends (infer U)[]
228
+ ? U extends Record<string, any>
229
+ ? UnwrapQueryableRecord<U>[] | Extract<R[K], undefined>
230
+ : never
231
+ : NonNullable<R[K]> extends Record<string, any>
232
+ ? UnwrapQueryableRecord<NonNullable<R[K]>> | Extract<R[K], undefined>
233
+ : never;
234
+ };
235
+ ```
236
+
237
+ ## `PathProxy`
238
+
239
+ include()에서 관계 경로를 타입 안전하게 지정하기 위한 프록시 타입. `ColumnPrimitive`가 아닌 필드(FK, FKT 관계)만 접근 가능하다.
240
+
241
+ ```typescript
242
+ export type PathProxy<TObject> = {
243
+ [K in keyof TObject as TObject[K] extends ColumnPrimitive ? never : K]-?: PathProxy<
244
+ UnwrapArray<TObject[K]>
245
+ >;
246
+ } & { readonly [PATH_SYMBOL]: string[] };
247
+ ```
248
+
249
+ ## `Executable`
250
+
251
+ Stored Procedure 실행 래퍼 클래스.
252
+
253
+ ```typescript
254
+ export class Executable<TParams extends ColumnBuilderRecord, TReturns extends ColumnBuilderRecord> {
255
+ constructor(db: DbContextBase, builder: ProcedureBuilder<TParams, TReturns>);
256
+
257
+ getExecProcQueryDef(params?: InferColumnExprs<TParams>): ExecProcQueryDef;
258
+ execute(params: InferColumnExprs<TParams>): Promise<InferColumnExprs<TReturns>[][]>;
259
+ }
260
+ ```
261
+
262
+ ## `executable`
263
+
264
+ Executable 생성 팩토리 함수.
265
+
266
+ ```typescript
267
+ export function executable<TParams extends ColumnBuilderRecord, TReturns extends ColumnBuilderRecord>(
268
+ db: DbContextBase,
269
+ builder: ProcedureBuilder<TParams, TReturns>,
270
+ ): () => Executable<TParams, TReturns>;
271
+ ```
272
+
273
+ ## `parseSearchQuery`
274
+
275
+ 검색 쿼리 문자열을 파싱하여 SQL LIKE 패턴으로 변환한다.
276
+
277
+ ```typescript
278
+ export function parseSearchQuery(searchText: string): ParsedSearchQuery;
279
+ ```
280
+
281
+ ### 검색 구문
282
+
283
+ | 구문 | 의미 | 예시 |
284
+ |------|------|------|
285
+ | `term1 term2` | OR (하나 이상 일치) | `apple banana` |
286
+ | `+term` | 필수 포함 (AND) | `+apple +banana` |
287
+ | `-term` | 제외 (NOT) | `apple -banana` |
288
+ | `"exact phrase"` | 정확한 구문 일치 (필수) | `"맛있는 과일"` |
289
+ | `*` | 와일드카드 | `app*` -> `app%` |
290
+
291
+ ### 이스케이프 시퀀스
292
+
293
+ `\\` (리터럴 `\`), `\*` (리터럴 `*`), `\%` (리터럴 `%`), `\"` (리터럴 `"`), `\+` (리터럴 `+`), `\-` (리터럴 `-`)
294
+
295
+ ## `ParsedSearchQuery`
296
+
297
+ 검색 쿼리 파싱 결과.
298
+
299
+ ```typescript
300
+ export interface ParsedSearchQuery {
301
+ or: string[];
302
+ must: string[];
303
+ not: string[];
304
+ }
305
+ ```
306
+
307
+ | Field | Type | Description |
308
+ |-------|------|-------------|
309
+ | `or` | `string[]` | 일반 검색어 (OR 조건) - LIKE 패턴 |
310
+ | `must` | `string[]` | 필수 검색어 (AND 조건, + 접두사 또는 따옴표) - LIKE 패턴 |
311
+ | `not` | `string[]` | 제외 검색어 (NOT 조건, - 접두사) - LIKE 패턴 |