@simplysm/orm-common 13.0.96 → 13.0.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -54
- package/docs/core.md +225 -0
- package/docs/expression.md +296 -0
- package/docs/query-builder.md +196 -0
- package/docs/queryable.md +578 -0
- package/docs/schema-builders.md +415 -0
- package/docs/types.md +445 -0
- package/docs/utilities.md +122 -0
- package/package.json +2 -2
- package/docs/ddl.md +0 -300
- package/docs/query.md +0 -644
- package/docs/schema.md +0 -325
package/docs/query.md
DELETED
|
@@ -1,644 +0,0 @@
|
|
|
1
|
-
# 쿼리 & 표현식
|
|
2
|
-
|
|
3
|
-
## Queryable -- SELECT 쿼리 빌더
|
|
4
|
-
|
|
5
|
-
### 기본 조회
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// 전체 조회
|
|
9
|
-
const users = await db.user().execute();
|
|
10
|
-
|
|
11
|
-
// 단건 조회 (2건 이상이면 throw)
|
|
12
|
-
const user = await db.user()
|
|
13
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
14
|
-
.single();
|
|
15
|
-
|
|
16
|
-
// 첫 번째 결과 (여러 건이어도 첫 번째만 반환)
|
|
17
|
-
const latest = await db.user()
|
|
18
|
-
.orderBy((c) => c.createdAt, "DESC")
|
|
19
|
-
.first();
|
|
20
|
-
|
|
21
|
-
// 행 수
|
|
22
|
-
const count = await db.user()
|
|
23
|
-
.where((c) => [expr.eq(c.isActive, true)])
|
|
24
|
-
.count();
|
|
25
|
-
|
|
26
|
-
// 존재 여부
|
|
27
|
-
const hasAdmin = await db.user()
|
|
28
|
-
.where((c) => [expr.eq(c.role, "admin")])
|
|
29
|
-
.exists();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Queryable API
|
|
33
|
-
|
|
34
|
-
#### 실행 메서드
|
|
35
|
-
|
|
36
|
-
| 메서드 | 시그니처 | 설명 |
|
|
37
|
-
|--------|---------|------|
|
|
38
|
-
| `execute` | `() => Promise<TData[]>` | SELECT 실행, 결과 배열 반환 |
|
|
39
|
-
| `single` | `() => Promise<TData \| undefined>` | 단건 반환 (2건 이상이면 throw) |
|
|
40
|
-
| `first` | `() => Promise<TData \| undefined>` | 첫 번째 결과 반환 |
|
|
41
|
-
| `count` | `(fn?) => Promise<number>` | 행 수 반환. distinct/groupBy 이후에는 wrap() 필요 |
|
|
42
|
-
| `exists` | `() => Promise<boolean>` | 데이터 존재 여부 |
|
|
43
|
-
|
|
44
|
-
#### 조건 메서드 (체이닝)
|
|
45
|
-
|
|
46
|
-
| 메서드 | 시그니처 | 설명 |
|
|
47
|
-
|--------|---------|------|
|
|
48
|
-
| `where` | `(fn: (cols) => WhereExprUnit[]) => Queryable` | WHERE 조건 (여러 번 호출 시 AND 결합) |
|
|
49
|
-
| `search` | `(fn: (cols) => ExprUnit[], text: string) => Queryable` | 텍스트 검색 |
|
|
50
|
-
| `orderBy` | `(fn: (cols) => ExprUnit, dir?) => Queryable` | 정렬 (기본 ASC, 여러 번 호출 가능) |
|
|
51
|
-
| `top` | `(count: number) => Queryable` | 상위 N건 (ORDER BY 없이 사용 가능) |
|
|
52
|
-
| `limit` | `(skip: number, take: number) => Queryable` | 페이징 (ORDER BY 필수) |
|
|
53
|
-
| `select` | `(fn: (cols) => Record) => Queryable` | 컬럼 선택/변환 |
|
|
54
|
-
| `distinct` | `() => Queryable` | 중복 제거 |
|
|
55
|
-
| `lock` | `() => Queryable` | FOR UPDATE 잠금 |
|
|
56
|
-
| `groupBy` | `(fn: (cols) => ExprUnit[]) => Queryable` | 그룹화 |
|
|
57
|
-
| `having` | `(fn: (cols) => WhereExprUnit[]) => Queryable` | 그룹 필터링 |
|
|
58
|
-
|
|
59
|
-
#### JOIN 메서드
|
|
60
|
-
|
|
61
|
-
| 메서드 | 시그니처 | 설명 |
|
|
62
|
-
|--------|---------|------|
|
|
63
|
-
| `join` | `(as, fn: (qr, cols) => Queryable) => Queryable` | LEFT JOIN (1:N, 배열) |
|
|
64
|
-
| `joinSingle` | `(as, fn: (qr, cols) => Queryable) => Queryable` | LEFT JOIN (N:1, 단일 객체) |
|
|
65
|
-
| `include` | `(fn: (item) => PathProxy) => Queryable` | 관계 기반 자동 JOIN |
|
|
66
|
-
|
|
67
|
-
#### 서브쿼리/유틸리티
|
|
68
|
-
|
|
69
|
-
| 메서드 | 시그니처 | 설명 |
|
|
70
|
-
|--------|---------|------|
|
|
71
|
-
| `wrap` | `() => Queryable` | 서브쿼리로 래핑 |
|
|
72
|
-
| `Queryable.union` | `(...queries) => Queryable` | UNION (최소 2개) |
|
|
73
|
-
| `recursive` | `(fn: (cte) => Queryable) => Queryable` | 재귀 CTE |
|
|
74
|
-
|
|
75
|
-
#### CUD 메서드
|
|
76
|
-
|
|
77
|
-
| 메서드 | 시그니처 | 설명 |
|
|
78
|
-
|--------|---------|------|
|
|
79
|
-
| `insert` | `(records, outputColumns?) => Promise` | INSERT (1000건 단위 자동 분할) |
|
|
80
|
-
| `insertIfNotExists` | `(record, outputColumns?) => Promise` | 조건부 INSERT |
|
|
81
|
-
| `insertInto` | `(targetTable, outputColumns?) => Promise` | INSERT INTO ... SELECT |
|
|
82
|
-
| `update` | `(fn: (cols) => Record, outputColumns?) => Promise` | UPDATE |
|
|
83
|
-
| `delete` | `(outputColumns?) => Promise` | DELETE |
|
|
84
|
-
| `upsert` | `(updateFn, insertFn?, outputColumns?) => Promise` | UPSERT (UPDATE or INSERT) |
|
|
85
|
-
| `switchFk` | `(enabled: boolean) => Promise<void>` | FK 제약조건 on/off |
|
|
86
|
-
|
|
87
|
-
### 필터링 (WHERE)
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
db.user()
|
|
91
|
-
.where((c) => [
|
|
92
|
-
expr.eq(c.name, "Alice"), // name = 'Alice'
|
|
93
|
-
expr.gt(c.score, 80), // score > 80
|
|
94
|
-
expr.between(c.createdAt, from, to), // BETWEEN
|
|
95
|
-
expr.like(c.email, "%@gmail.com"), // LIKE
|
|
96
|
-
expr.in(c.role, ["admin", "user"]), // IN
|
|
97
|
-
expr.null(c.deletedAt), // IS NULL
|
|
98
|
-
])
|
|
99
|
-
// 배열은 자동 AND 결합. OR는 명시적으로:
|
|
100
|
-
.where((c) => [
|
|
101
|
-
expr.or([
|
|
102
|
-
expr.eq(c.role, "admin"),
|
|
103
|
-
expr.gt(c.score, 90),
|
|
104
|
-
]),
|
|
105
|
-
])
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
where()를 여러 번 호출하면 AND로 결합된다.
|
|
109
|
-
|
|
110
|
-
### 텍스트 검색 (search)
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
// 여러 컬럼에서 텍스트 검색
|
|
114
|
-
db.user()
|
|
115
|
-
.search((c) => [c.name, c.email], "John +admin -withdrawn")
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
검색 문법: `term`(OR), `+term`(AND 필수), `-term`(NOT 제외), `"exact"`(정확히 + 필수), `wild*`(접두사). 내부적으로 `parseSearchQuery()`를 사용하여 SQL LIKE 패턴으로 변환한다.
|
|
119
|
-
|
|
120
|
-
### 정렬 (ORDER BY)
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
// orderBy는 컬럼 하나씩, 여러 번 호출 가능
|
|
124
|
-
db.user()
|
|
125
|
-
.orderBy((c) => c.createdAt, "DESC")
|
|
126
|
-
.orderBy((c) => c.name) // 기본: ASC
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### 페이징
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
// top: ORDER BY 없이도 사용 가능
|
|
133
|
-
db.user().top(10)
|
|
134
|
-
|
|
135
|
-
// limit: ORDER BY 필수 (skip, take)
|
|
136
|
-
db.user()
|
|
137
|
-
.orderBy((c) => c.createdAt, "DESC")
|
|
138
|
-
.limit(20, 10) // 20건 건너뛰고 10건 가져오기
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### 컬럼 선택 (SELECT)
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
db.user()
|
|
145
|
-
.select((c) => ({
|
|
146
|
-
id: c.id,
|
|
147
|
-
upperName: expr.upper(c.name),
|
|
148
|
-
emailDomain: expr.right(c.email, 10),
|
|
149
|
-
}))
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### 그룹화 (GROUP BY)
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
db.order()
|
|
156
|
-
.select((c) => ({
|
|
157
|
-
userId: c.userId,
|
|
158
|
-
total: expr.sum(c.amount),
|
|
159
|
-
count: expr.count(),
|
|
160
|
-
avg: expr.avg(c.amount),
|
|
161
|
-
}))
|
|
162
|
-
.groupBy((c) => [c.userId])
|
|
163
|
-
.having((c) => [expr.gt(expr.count(), 5)])
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### JOIN
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
// LEFT JOIN (1:N, 배열로 결과에 추가)
|
|
170
|
-
db.user()
|
|
171
|
-
.join("posts", (qr, u) =>
|
|
172
|
-
qr.from(Post)
|
|
173
|
-
.where((p) => [expr.eq(p.userId, u.id)])
|
|
174
|
-
)
|
|
175
|
-
// 결과: { id, name, posts: [{ id, title }, ...] }
|
|
176
|
-
|
|
177
|
-
// LEFT JOIN SINGLE (N:1, 단일 객체로 결과에 추가)
|
|
178
|
-
db.order()
|
|
179
|
-
.joinSingle("user", (qr, o) =>
|
|
180
|
-
qr.from(User)
|
|
181
|
-
.where((u) => [expr.eq(u.id, o.userId)])
|
|
182
|
-
)
|
|
183
|
-
// 결과: { id, amount, user: { id, name } | undefined }
|
|
184
|
-
|
|
185
|
-
// 관계 자동 로드 (include) -- 정의된 관계 기반 자동 JOIN
|
|
186
|
-
db.order()
|
|
187
|
-
.include((c) => c.user) // N:1 관계 로드
|
|
188
|
-
.include((c) => c.user.company) // 중첩 관계
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### 서브쿼리
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
// 스칼라 서브쿼리
|
|
195
|
-
db.user().select((c) => ({
|
|
196
|
-
name: c.name,
|
|
197
|
-
orderCount: expr.subquery("number",
|
|
198
|
-
db.order()
|
|
199
|
-
.where((o) => [expr.eq(o.userId, c.id)])
|
|
200
|
-
.select(() => ({ cnt: expr.count() }))
|
|
201
|
-
),
|
|
202
|
-
}))
|
|
203
|
-
|
|
204
|
-
// IN 서브쿼리
|
|
205
|
-
db.user().where((c) => [
|
|
206
|
-
expr.inQuery(c.id,
|
|
207
|
-
db.order()
|
|
208
|
-
.where((o) => [expr.gt(o.amount, 1000)])
|
|
209
|
-
.select((o) => ({ userId: o.userId }))
|
|
210
|
-
),
|
|
211
|
-
])
|
|
212
|
-
|
|
213
|
-
// EXISTS
|
|
214
|
-
db.user().where((c) => [
|
|
215
|
-
expr.exists(db.order().where((o) => [expr.eq(o.userId, c.id)])),
|
|
216
|
-
])
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### 잠금 (FOR UPDATE)
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
await db.connect(async () => {
|
|
223
|
-
const user = await db.user()
|
|
224
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
225
|
-
.lock()
|
|
226
|
-
.single();
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### DISTINCT
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
db.user().select((c) => ({ role: c.role })).distinct()
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### 서브쿼리 래핑 (wrap)
|
|
237
|
-
|
|
238
|
-
DISTINCT나 GROUP BY 이후 count() 등을 사용하려면 wrap()으로 서브쿼리화해야 한다.
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
const count = await db.user()
|
|
242
|
-
.select((c) => ({ name: c.name }))
|
|
243
|
-
.distinct()
|
|
244
|
-
.wrap()
|
|
245
|
-
.count();
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### UNION
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
const combined = Queryable.union(
|
|
252
|
-
db.user().where((c) => [expr.eq(c.type, "admin")]),
|
|
253
|
-
db.user().where((c) => [expr.eq(c.type, "manager")]),
|
|
254
|
-
);
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### 재귀 쿼리 (CTE)
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
// 계층 데이터 조회 (조직도, 카테고리 트리 등)
|
|
261
|
-
db.employee()
|
|
262
|
-
.where((e) => [expr.null(e.managerId)]) // 루트 노드
|
|
263
|
-
.recursive((cte) =>
|
|
264
|
-
cte.from(Employee)
|
|
265
|
-
.where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
266
|
-
)
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## CUD 연산
|
|
272
|
-
|
|
273
|
-
`select()`, `groupBy()`, `join()` 이후에는 CUD 불가 (타입 레벨에서 차단).
|
|
274
|
-
|
|
275
|
-
### INSERT
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
// 기본 삽입
|
|
279
|
-
await db.user().insert([
|
|
280
|
-
{ name: "Alice", email: "alice@example.com", createdAt: new DateTime() },
|
|
281
|
-
{ name: "Bob", createdAt: new DateTime() },
|
|
282
|
-
]);
|
|
283
|
-
|
|
284
|
-
// INSERT 후 컬럼 반환 (OUTPUT)
|
|
285
|
-
const [inserted] = await db.user().insert(
|
|
286
|
-
[{ name: "Alice", createdAt: new DateTime() }],
|
|
287
|
-
["id"], // 반환받을 컬럼
|
|
288
|
-
);
|
|
289
|
-
// inserted.id -> 자동 생성된 ID
|
|
290
|
-
|
|
291
|
-
// 조건부 INSERT (WHERE 조건에 맞는 데이터가 없을 때만)
|
|
292
|
-
await db.user()
|
|
293
|
-
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
294
|
-
.insertIfNotExists({ name: "testing", email: "test@test.com", createdAt: new DateTime() });
|
|
295
|
-
|
|
296
|
-
// INSERT INTO ... SELECT
|
|
297
|
-
await db.user()
|
|
298
|
-
.select((c) => ({ name: c.name, createdAt: c.createdAt }))
|
|
299
|
-
.where((c) => [expr.eq(c.isArchived, false)])
|
|
300
|
-
.insertInto(ArchivedUser);
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### UPDATE
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
await db.user()
|
|
307
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
308
|
-
.update((c) => ({ name: expr.val("string", "Alice2") }));
|
|
309
|
-
|
|
310
|
-
// 기존 값 참조
|
|
311
|
-
await db.product()
|
|
312
|
-
.update((p) => ({
|
|
313
|
-
price: expr.mul(p.price, expr.val("number", 1.1)),
|
|
314
|
-
}));
|
|
315
|
-
|
|
316
|
-
// OUTPUT으로 변경된 데이터 반환
|
|
317
|
-
const updated = await db.user()
|
|
318
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
319
|
-
.update(
|
|
320
|
-
(c) => ({ name: expr.val("string", "Alice2") }),
|
|
321
|
-
["id", "name"],
|
|
322
|
-
);
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### DELETE
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
await db.user()
|
|
329
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
330
|
-
.delete();
|
|
331
|
-
|
|
332
|
-
// OUTPUT으로 삭제된 데이터 반환
|
|
333
|
-
const deleted = await db.user()
|
|
334
|
-
.where((c) => [expr.eq(c.isExpired, true)])
|
|
335
|
-
.delete(["id", "name"]);
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### UPSERT (UPDATE or INSERT)
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
// 동일 데이터로 UPDATE/INSERT
|
|
342
|
-
await db.user()
|
|
343
|
-
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
344
|
-
.upsert(() => ({
|
|
345
|
-
name: expr.val("string", "testing"),
|
|
346
|
-
email: expr.val("string", "test@test.com"),
|
|
347
|
-
}));
|
|
348
|
-
|
|
349
|
-
// UPDATE/INSERT 데이터가 다른 경우
|
|
350
|
-
await db.user()
|
|
351
|
-
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
352
|
-
.upsert(
|
|
353
|
-
() => ({ loginCount: expr.val("number", 1) }), // UPDATE용
|
|
354
|
-
(update) => ({ ...update, email: expr.val("string", "test@test.com") }), // INSERT용
|
|
355
|
-
);
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
### FK 토글
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
// Queryable에서 직접 FK on/off
|
|
362
|
-
await db.user().switchFk(false); // FK 비활성화
|
|
363
|
-
// ... 벌크 작업 ...
|
|
364
|
-
await db.user().switchFk(true); // FK 활성화
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
---
|
|
368
|
-
|
|
369
|
-
## Executable -- 프로시저 실행
|
|
370
|
-
|
|
371
|
-
```typescript
|
|
372
|
-
const MyDb = defineDbContext({
|
|
373
|
-
tables: { user: User },
|
|
374
|
-
procedures: { getUserOrders: GetUserOrders },
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
const db = createDbContext(MyDb, executor, { database: "mydb" });
|
|
378
|
-
|
|
379
|
-
await db.connect(async () => {
|
|
380
|
-
const results = await db.getUserOrders().execute({ userId: 1 });
|
|
381
|
-
});
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### Executable API
|
|
385
|
-
|
|
386
|
-
| 메서드 | 시그니처 | 설명 |
|
|
387
|
-
|--------|---------|------|
|
|
388
|
-
| `execute` | `(params) => Promise<TReturns[][]>` | 프로시저 실행 |
|
|
389
|
-
| `getExecProcQueryDef` | `(params?) => QueryDef` | QueryDef만 생성 (실행 안 함) |
|
|
390
|
-
|
|
391
|
-
---
|
|
392
|
-
|
|
393
|
-
## expr -- 표현식 빌더
|
|
394
|
-
|
|
395
|
-
방언 독립적인 SQL 표현식을 생성한다. JSON AST(Expr)를 생성하며, QueryBuilder가 각 DBMS(MySQL, MSSQL, PostgreSQL)에 맞게 변환한다.
|
|
396
|
-
|
|
397
|
-
### 값/조건
|
|
398
|
-
|
|
399
|
-
| 함수 | 설명 |
|
|
400
|
-
|------|------|
|
|
401
|
-
| `expr.val(type, value)` | 리터럴 값 |
|
|
402
|
-
| `expr.col(type, alias, key)` | 컬럼 참조 |
|
|
403
|
-
| `expr.raw(type)\`sql\`` | Raw SQL (태그 템플릿) |
|
|
404
|
-
| `expr.eq(a, b)` | `=` (NULL 안전) |
|
|
405
|
-
| `expr.gt(a, b)` | `>` |
|
|
406
|
-
| `expr.lt(a, b)` | `<` |
|
|
407
|
-
| `expr.gte(a, b)` | `>=` |
|
|
408
|
-
| `expr.lte(a, b)` | `<=` |
|
|
409
|
-
| `expr.between(src, from?, to?)` | BETWEEN |
|
|
410
|
-
| `expr.null(src)` | IS NULL |
|
|
411
|
-
| `expr.like(src, pattern)` | LIKE |
|
|
412
|
-
| `expr.regexp(src, pattern)` | 정규식 매칭 |
|
|
413
|
-
| `expr.in(src, values)` | IN (값 목록) |
|
|
414
|
-
| `expr.inQuery(src, query)` | IN (서브쿼리) |
|
|
415
|
-
| `expr.exists(query)` | EXISTS |
|
|
416
|
-
| `expr.not(cond)` | NOT |
|
|
417
|
-
| `expr.and(conds)` | AND |
|
|
418
|
-
| `expr.or(conds)` | OR |
|
|
419
|
-
|
|
420
|
-
### 문자열 함수
|
|
421
|
-
|
|
422
|
-
| 함수 | 설명 |
|
|
423
|
-
|------|------|
|
|
424
|
-
| `expr.concat(...args)` | 연결 (NULL=빈문자열) |
|
|
425
|
-
| `expr.left(src, len)` | 왼쪽 N자 |
|
|
426
|
-
| `expr.right(src, len)` | 오른쪽 N자 |
|
|
427
|
-
| `expr.substring(src, start, len?)` | 부분 문자열 (1-based) |
|
|
428
|
-
| `expr.trim(src)` | 양쪽 공백 제거 |
|
|
429
|
-
| `expr.padStart(src, len, fill)` | 왼쪽 패딩 (LPAD) |
|
|
430
|
-
| `expr.replace(src, from, to)` | 치환 |
|
|
431
|
-
| `expr.upper(src)` / `expr.lower(src)` | 대/소문자 |
|
|
432
|
-
| `expr.length(src)` | 문자 길이 (CHAR_LENGTH) |
|
|
433
|
-
| `expr.byteLength(src)` | 바이트 길이 (LENGTH/DATALENGTH) |
|
|
434
|
-
| `expr.indexOf(src, search)` | 위치 (1-based, 0=없음) |
|
|
435
|
-
|
|
436
|
-
### 수학 함수
|
|
437
|
-
|
|
438
|
-
| 함수 | 설명 |
|
|
439
|
-
|------|------|
|
|
440
|
-
| `expr.abs(src)` | 절대값 |
|
|
441
|
-
| `expr.round(src, digits)` | 반올림 |
|
|
442
|
-
| `expr.ceil(src)` / `expr.floor(src)` | 올림/내림 |
|
|
443
|
-
|
|
444
|
-
### 날짜 함수
|
|
445
|
-
|
|
446
|
-
| 함수 | 설명 |
|
|
447
|
-
|------|------|
|
|
448
|
-
| `expr.year(src)` / `expr.month(src)` / `expr.day(src)` | 연/월/일 추출 |
|
|
449
|
-
| `expr.hour(src)` / `expr.minute(src)` / `expr.second(src)` | 시/분/초 추출 |
|
|
450
|
-
| `expr.dateDiff(unit, from, to)` | 날짜 차이 |
|
|
451
|
-
| `expr.dateAdd(unit, src, value)` | 날짜 더하기 |
|
|
452
|
-
| `expr.formatDate(src, format)` | 날짜 포맷 (FORMAT/DATE_FORMAT) |
|
|
453
|
-
| `expr.isoWeek(src)` | ISO 주차 |
|
|
454
|
-
| `expr.isoWeekStartDate(src)` | ISO 주 시작일 |
|
|
455
|
-
| `expr.isoYearMonth(src)` | ISO 연월 (YYYYMM 형식) |
|
|
456
|
-
|
|
457
|
-
단위(`DateUnit`): `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`
|
|
458
|
-
|
|
459
|
-
### 집계 함수
|
|
460
|
-
|
|
461
|
-
| 함수 | 설명 |
|
|
462
|
-
|------|------|
|
|
463
|
-
| `expr.count(arg?, distinct?)` | 행 수 |
|
|
464
|
-
| `expr.sum(arg)` | 합계 |
|
|
465
|
-
| `expr.avg(arg)` | 평균 |
|
|
466
|
-
| `expr.max(arg)` / `expr.min(arg)` | 최대/최소 |
|
|
467
|
-
|
|
468
|
-
### 조건 함수
|
|
469
|
-
|
|
470
|
-
```typescript
|
|
471
|
-
// CASE WHEN
|
|
472
|
-
expr.switch<string>()
|
|
473
|
-
.case(expr.gt(c.score, 90), "A")
|
|
474
|
-
.case(expr.gt(c.score, 80), "B")
|
|
475
|
-
.default("C")
|
|
476
|
-
|
|
477
|
-
// IF (단순 삼항)
|
|
478
|
-
expr.if(expr.gt(c.score, 60), "pass", "fail")
|
|
479
|
-
|
|
480
|
-
// WHERE -> boolean 컬럼
|
|
481
|
-
expr.is(expr.gt(c.score, 80)) // true/false
|
|
482
|
-
|
|
483
|
-
// NULLIF (source === value이면 NULL 반환)
|
|
484
|
-
expr.nullIf(c.status, "unknown")
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
### 윈도우 함수
|
|
488
|
-
|
|
489
|
-
```typescript
|
|
490
|
-
// 순위
|
|
491
|
-
expr.rowNumber({ partitionBy: [c.dept], orderBy: [[c.score, "DESC"]] })
|
|
492
|
-
expr.rank({ orderBy: [[c.score, "DESC"]] })
|
|
493
|
-
expr.denseRank({ orderBy: [[c.score, "DESC"]] })
|
|
494
|
-
expr.ntile(4, { orderBy: [[c.score, "DESC"]] })
|
|
495
|
-
|
|
496
|
-
// 탐색
|
|
497
|
-
expr.lag(c.score, 1, 0, { orderBy: [[c.createdAt, "ASC"]] })
|
|
498
|
-
expr.lead(c.score, 1, undefined, { orderBy: [[c.createdAt, "ASC"]] })
|
|
499
|
-
expr.firstValue(c.score, { orderBy: [[c.createdAt, "ASC"]] })
|
|
500
|
-
expr.lastValue(c.score, { orderBy: [[c.createdAt, "ASC"]] })
|
|
501
|
-
|
|
502
|
-
// 윈도우 집계
|
|
503
|
-
expr.sumOver(c.amount, { partitionBy: [c.dept] })
|
|
504
|
-
expr.avgOver(c.amount, { partitionBy: [c.dept] })
|
|
505
|
-
expr.countOver({ partitionBy: [c.dept] })
|
|
506
|
-
expr.minOver(c.amount, { partitionBy: [c.dept] })
|
|
507
|
-
expr.maxOver(c.amount, { partitionBy: [c.dept] })
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
### 기타
|
|
511
|
-
|
|
512
|
-
| 함수 | 설명 |
|
|
513
|
-
|------|------|
|
|
514
|
-
| `expr.cast(src, targetType)` | 타입 변환 |
|
|
515
|
-
| `expr.greatest(...args)` | 최대값 |
|
|
516
|
-
| `expr.least(...args)` | 최소값 |
|
|
517
|
-
| `expr.rowNum()` | 행 번호 (전체) |
|
|
518
|
-
| `expr.random()` | 랜덤 0~1 |
|
|
519
|
-
| `expr.subquery(type, queryable)` | 스칼라 서브쿼리 |
|
|
520
|
-
|
|
521
|
-
---
|
|
522
|
-
|
|
523
|
-
## ExprUnit / WhereExprUnit
|
|
524
|
-
|
|
525
|
-
Queryable 콜백에서 컬럼 참조 시 자동으로 `ExprUnit`으로 래핑된다.
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
// ExprUnit<TPrimitive> -- 타입 안전한 표현식 래퍼
|
|
529
|
-
class ExprUnit<TPrimitive extends ColumnPrimitive> {
|
|
530
|
-
readonly dataType: ColumnPrimitiveStr;
|
|
531
|
-
readonly expr: Expr;
|
|
532
|
-
get n(): ExprUnit<NonNullable<TPrimitive>>; // nullable 제거
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// WhereExprUnit -- WHERE 절용 표현식 래퍼
|
|
536
|
-
class WhereExprUnit {
|
|
537
|
-
readonly expr: WhereExpr;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// ExprInput -- ExprUnit 또는 리터럴 값을 받는 입력 타입
|
|
541
|
-
type ExprInput<TPrimitive> = ExprUnit<TPrimitive> | TPrimitive;
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
---
|
|
545
|
-
|
|
546
|
-
## 검색 파서
|
|
547
|
-
|
|
548
|
-
사용자 검색 문법을 SQL LIKE 패턴으로 변환.
|
|
549
|
-
|
|
550
|
-
```typescript
|
|
551
|
-
import { parseSearchQuery } from "@simplysm/orm-common";
|
|
552
|
-
|
|
553
|
-
parseSearchQuery('apple +fruit -rotten "green apple" wild*');
|
|
554
|
-
// {
|
|
555
|
-
// or: ["%apple%", "wild%"],
|
|
556
|
-
// must: ["%green apple%", "%fruit%"],
|
|
557
|
-
// not: ["%rotten%"],
|
|
558
|
-
// }
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
### parseSearchQuery API
|
|
562
|
-
|
|
563
|
-
```
|
|
564
|
-
parseSearchQuery(searchText: string): ParsedSearchQuery
|
|
565
|
-
|
|
566
|
-
interface ParsedSearchQuery {
|
|
567
|
-
or: string[]; // OR 조건 (LIKE 패턴)
|
|
568
|
-
must: string[]; // AND 필수 조건 (LIKE 패턴)
|
|
569
|
-
not: string[]; // NOT 제외 조건 (LIKE 패턴)
|
|
570
|
-
}
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
문법: `term`(OR), `+term`(AND 필수), `-term`(NOT 제외), `"exact"`(정확히 + 필수), `wild*`(접두사)
|
|
574
|
-
|
|
575
|
-
이스케이프: `\\` (리터럴 `\`), `\*` (리터럴 `*`), `\%` (리터럴 `%`), `\"` (리터럴 `"`), `\+` (리터럴 `+`), `\-` (리터럴 `-`)
|
|
576
|
-
|
|
577
|
-
---
|
|
578
|
-
|
|
579
|
-
## QueryBuilder
|
|
580
|
-
|
|
581
|
-
QueryDef(JSON AST)를 DBMS별 SQL 문자열로 변환한다.
|
|
582
|
-
|
|
583
|
-
```typescript
|
|
584
|
-
import { createQueryBuilder } from "@simplysm/orm-common";
|
|
585
|
-
import type { Dialect } from "@simplysm/orm-common";
|
|
586
|
-
|
|
587
|
-
const builder = createQueryBuilder("mysql"); // "mysql" | "mssql" | "postgresql"
|
|
588
|
-
const result = builder.build(queryDef);
|
|
589
|
-
// result.sql -> SQL 문자열
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### createQueryBuilder API
|
|
593
|
-
|
|
594
|
-
```
|
|
595
|
-
createQueryBuilder(dialect: Dialect): QueryBuilderBase
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
| 방언 | 구현 클래스 |
|
|
599
|
-
|------|-----------|
|
|
600
|
-
| `"mysql"` | `MysqlQueryBuilder` |
|
|
601
|
-
| `"mssql"` | `MssqlQueryBuilder` |
|
|
602
|
-
| `"postgresql"` | `PostgresqlQueryBuilder` |
|
|
603
|
-
|
|
604
|
-
---
|
|
605
|
-
|
|
606
|
-
## parseQueryResult
|
|
607
|
-
|
|
608
|
-
DB 쿼리 결과를 ResultMeta 기반으로 TypeScript 객체로 변환한다. 타입 파싱과 JOIN 결과 중첩을 처리한다.
|
|
609
|
-
|
|
610
|
-
```typescript
|
|
611
|
-
import { parseQueryResult } from "@simplysm/orm-common";
|
|
612
|
-
|
|
613
|
-
// 단순 타입 파싱
|
|
614
|
-
const raw = [{ id: "1", createdAt: "2026-01-07T10:00:00.000Z" }];
|
|
615
|
-
const meta = { columns: { id: "number", createdAt: "DateTime" }, joins: {} };
|
|
616
|
-
const result = await parseQueryResult(raw, meta);
|
|
617
|
-
// [{ id: 1, createdAt: DateTime(...) }]
|
|
618
|
-
|
|
619
|
-
// JOIN 결과 중첩
|
|
620
|
-
const raw2 = [
|
|
621
|
-
{ id: 1, name: "User1", "posts.id": 10, "posts.title": "Post1" },
|
|
622
|
-
{ id: 1, name: "User1", "posts.id": 11, "posts.title": "Post2" },
|
|
623
|
-
];
|
|
624
|
-
const meta2 = {
|
|
625
|
-
columns: { id: "number", name: "string", "posts.id": "number", "posts.title": "string" },
|
|
626
|
-
joins: { posts: { isSingle: false } },
|
|
627
|
-
};
|
|
628
|
-
const result2 = await parseQueryResult(raw2, meta2);
|
|
629
|
-
// [{ id: 1, name: "User1", posts: [{ id: 10, title: "Post1" }, { id: 11, title: "Post2" }] }]
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
### parseQueryResult API
|
|
633
|
-
|
|
634
|
-
```
|
|
635
|
-
parseQueryResult<TRecord>(
|
|
636
|
-
rawResults: Record<string, unknown>[],
|
|
637
|
-
meta: ResultMeta,
|
|
638
|
-
): Promise<TRecord[] | undefined>
|
|
639
|
-
|
|
640
|
-
interface ResultMeta {
|
|
641
|
-
columns: Record<string, ColumnPrimitiveStr>;
|
|
642
|
-
joins: Record<string, { isSingle: boolean }>;
|
|
643
|
-
}
|
|
644
|
-
```
|