@simplysm/orm-common 13.0.85 → 13.0.86
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 +65 -59
- package/docs/ddl.md +195 -0
- package/docs/query.md +469 -0
- package/docs/schema.md +238 -0
- package/package.json +2 -2
- package/docs/db-context.md +0 -238
- package/docs/expressions.md +0 -413
- package/docs/query-builder.md +0 -198
- package/docs/queryable.md +0 -420
- package/docs/schema-builders.md +0 -216
- package/docs/types-and-utilities.md +0 -353
package/docs/query.md
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
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
|
+
### 필터링 (WHERE)
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
db.user()
|
|
36
|
+
.where((c) => [
|
|
37
|
+
expr.eq(c.name, "Alice"), // name = 'Alice'
|
|
38
|
+
expr.gt(c.score, 80), // score > 80
|
|
39
|
+
expr.between(c.createdAt, from, to), // BETWEEN
|
|
40
|
+
expr.like(c.email, "%@gmail.com"), // LIKE
|
|
41
|
+
expr.in(c.role, ["admin", "user"]), // IN
|
|
42
|
+
expr.null(c.deletedAt), // IS NULL
|
|
43
|
+
])
|
|
44
|
+
// 배열은 자동 AND 결합. OR는 명시적으로:
|
|
45
|
+
.where((c) => [
|
|
46
|
+
expr.or([
|
|
47
|
+
expr.eq(c.role, "admin"),
|
|
48
|
+
expr.gt(c.score, 90),
|
|
49
|
+
]),
|
|
50
|
+
])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
where()를 여러 번 호출하면 AND로 결합된다.
|
|
54
|
+
|
|
55
|
+
### 텍스트 검색 (search)
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// 여러 컬럼에서 텍스트 검색
|
|
59
|
+
db.user()
|
|
60
|
+
.search((c) => [c.name, c.email], "John +admin -withdrawn")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
검색 문법: `term`(OR), `+term`(AND 필수), `-term`(NOT 제외), `"exact"`(정확히), `wild*`(접두사). 내부적으로 `parseSearchQuery()`를 사용하여 SQL LIKE 패턴으로 변환한다.
|
|
64
|
+
|
|
65
|
+
### 정렬 (ORDER BY)
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// orderBy는 컬럼 하나씩, 여러 번 호출 가능
|
|
69
|
+
db.user()
|
|
70
|
+
.orderBy((c) => c.createdAt, "DESC")
|
|
71
|
+
.orderBy((c) => c.name) // 기본: ASC
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 페이징
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// top: ORDER BY 없이도 사용 가능
|
|
78
|
+
db.user().top(10)
|
|
79
|
+
|
|
80
|
+
// limit: ORDER BY 필수 (skip, take)
|
|
81
|
+
db.user()
|
|
82
|
+
.orderBy((c) => c.createdAt, "DESC")
|
|
83
|
+
.limit(20, 10) // 20건 건너뛰고 10건 가져오기
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 컬럼 선택 (SELECT)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
db.user()
|
|
90
|
+
.select((c) => ({
|
|
91
|
+
id: c.id,
|
|
92
|
+
upperName: expr.upper(c.name),
|
|
93
|
+
emailDomain: expr.right(c.email, 10),
|
|
94
|
+
}))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 그룹화 (GROUP BY)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
db.order()
|
|
101
|
+
.select((c) => ({
|
|
102
|
+
userId: c.userId,
|
|
103
|
+
total: expr.sum(c.amount),
|
|
104
|
+
count: expr.count(),
|
|
105
|
+
avg: expr.avg(c.amount),
|
|
106
|
+
}))
|
|
107
|
+
.groupBy((c) => [c.userId])
|
|
108
|
+
.having((c) => [expr.gt(expr.count(), 5)])
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### JOIN
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// LEFT JOIN (1:N, 배열로 결과에 추가)
|
|
115
|
+
db.user()
|
|
116
|
+
.join("posts", (qr, u) =>
|
|
117
|
+
qr.from(Post)
|
|
118
|
+
.where((p) => [expr.eq(p.userId, u.id)])
|
|
119
|
+
)
|
|
120
|
+
// 결과: { id, name, posts: [{ id, title }, ...] }
|
|
121
|
+
|
|
122
|
+
// LEFT JOIN SINGLE (N:1, 단일 객체로 결과에 추가)
|
|
123
|
+
db.order()
|
|
124
|
+
.joinSingle("user", (qr, o) =>
|
|
125
|
+
qr.from(User)
|
|
126
|
+
.where((u) => [expr.eq(u.id, o.userId)])
|
|
127
|
+
)
|
|
128
|
+
// 결과: { id, amount, user: { id, name } | undefined }
|
|
129
|
+
|
|
130
|
+
// 관계 자동 로드 (include) -- 정의된 관계 기반 자동 JOIN
|
|
131
|
+
db.order()
|
|
132
|
+
.include((c) => c.user) // N:1 관계 로드
|
|
133
|
+
.include((c) => c.user.company) // 중첩 관계
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 서브쿼리
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// 스칼라 서브쿼리
|
|
140
|
+
db.user().select((c) => ({
|
|
141
|
+
name: c.name,
|
|
142
|
+
orderCount: expr.subquery("number",
|
|
143
|
+
db.order()
|
|
144
|
+
.where((o) => [expr.eq(o.userId, c.id)])
|
|
145
|
+
.select(() => ({ cnt: expr.count() }))
|
|
146
|
+
),
|
|
147
|
+
}))
|
|
148
|
+
|
|
149
|
+
// IN 서브쿼리
|
|
150
|
+
db.user().where((c) => [
|
|
151
|
+
expr.inQuery(c.id,
|
|
152
|
+
db.order()
|
|
153
|
+
.where((o) => [expr.gt(o.amount, 1000)])
|
|
154
|
+
.select((o) => ({ userId: o.userId }))
|
|
155
|
+
),
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
// EXISTS
|
|
159
|
+
db.user().where((c) => [
|
|
160
|
+
expr.exists(db.order().where((o) => [expr.eq(o.userId, c.id)])),
|
|
161
|
+
])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 잠금 (FOR UPDATE)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
await db.connect(async () => {
|
|
168
|
+
const user = await db.user()
|
|
169
|
+
.where((c) => [expr.eq(c.id, 1)])
|
|
170
|
+
.lock()
|
|
171
|
+
.single();
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### DISTINCT
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
db.user().select((c) => ({ role: c.role })).distinct()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 서브쿼리 래핑 (wrap)
|
|
182
|
+
|
|
183
|
+
DISTINCT나 GROUP BY 이후 count() 등을 사용하려면 wrap()으로 서브쿼리화해야 한다.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const count = await db.user()
|
|
187
|
+
.select((c) => ({ name: c.name }))
|
|
188
|
+
.distinct()
|
|
189
|
+
.wrap()
|
|
190
|
+
.count();
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### UNION
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const combined = Queryable.union(
|
|
197
|
+
db.user().where((c) => [expr.eq(c.type, "admin")]),
|
|
198
|
+
db.user().where((c) => [expr.eq(c.type, "manager")]),
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 재귀 쿼리 (CTE)
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// 계층 데이터 조회 (조직도, 카테고리 트리 등)
|
|
206
|
+
db.employee()
|
|
207
|
+
.where((e) => [expr.null(e.managerId)]) // 루트 노드
|
|
208
|
+
.recursive((cte) =>
|
|
209
|
+
cte.from(Employee)
|
|
210
|
+
.where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## CUD 연산
|
|
217
|
+
|
|
218
|
+
`select()`, `groupBy()`, `join()` 이후에는 CUD 불가 (타입 레벨에서 차단).
|
|
219
|
+
|
|
220
|
+
### INSERT
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// 기본 삽입
|
|
224
|
+
await db.user().insert([
|
|
225
|
+
{ name: "Alice", email: "alice@example.com", createdAt: new DateTime() },
|
|
226
|
+
{ name: "Bob", createdAt: new DateTime() },
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
// INSERT 후 컬럼 반환 (OUTPUT)
|
|
230
|
+
const [inserted] = await db.user().insert(
|
|
231
|
+
[{ name: "Alice", createdAt: new DateTime() }],
|
|
232
|
+
["id"], // 반환받을 컬럼
|
|
233
|
+
);
|
|
234
|
+
// inserted.id -> 자동 생성된 ID
|
|
235
|
+
|
|
236
|
+
// 조건부 INSERT (WHERE 조건에 맞는 데이터가 없을 때만)
|
|
237
|
+
await db.user()
|
|
238
|
+
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
239
|
+
.insertIfNotExists({ name: "testing", email: "test@test.com", createdAt: new DateTime() });
|
|
240
|
+
|
|
241
|
+
// INSERT INTO ... SELECT
|
|
242
|
+
await db.user()
|
|
243
|
+
.select((c) => ({ name: c.name, createdAt: c.createdAt }))
|
|
244
|
+
.where((c) => [expr.eq(c.isArchived, false)])
|
|
245
|
+
.insertInto(ArchivedUser);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### UPDATE
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
await db.user()
|
|
252
|
+
.where((c) => [expr.eq(c.id, 1)])
|
|
253
|
+
.update((c) => ({ name: expr.val("string", "Alice2") }));
|
|
254
|
+
|
|
255
|
+
// OUTPUT으로 변경된 데이터 반환
|
|
256
|
+
const updated = await db.user()
|
|
257
|
+
.where((c) => [expr.eq(c.id, 1)])
|
|
258
|
+
.update(
|
|
259
|
+
(c) => ({ name: expr.val("string", "Alice2") }),
|
|
260
|
+
["id", "name"],
|
|
261
|
+
);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### DELETE
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
await db.user()
|
|
268
|
+
.where((c) => [expr.eq(c.id, 1)])
|
|
269
|
+
.delete();
|
|
270
|
+
|
|
271
|
+
// OUTPUT으로 삭제된 데이터 반환
|
|
272
|
+
const deleted = await db.user()
|
|
273
|
+
.where((c) => [expr.eq(c.isExpired, true)])
|
|
274
|
+
.delete(["id", "name"]);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### UPSERT (UPDATE or INSERT)
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// 동일 데이터로 UPDATE/INSERT
|
|
281
|
+
await db.user()
|
|
282
|
+
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
283
|
+
.upsert(() => ({
|
|
284
|
+
name: expr.val("string", "testing"),
|
|
285
|
+
email: expr.val("string", "test@test.com"),
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
// UPDATE/INSERT 데이터가 다른 경우
|
|
289
|
+
await db.user()
|
|
290
|
+
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
291
|
+
.upsert(
|
|
292
|
+
() => ({ loginCount: expr.val("number", 1) }), // UPDATE용
|
|
293
|
+
(update) => ({ ...update, email: expr.val("string", "test@test.com") }), // INSERT용
|
|
294
|
+
);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### FK 토글
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Queryable에서 직접 FK on/off
|
|
301
|
+
await db.user().switchFk(false); // FK 비활성화
|
|
302
|
+
// ... 벌크 작업 ...
|
|
303
|
+
await db.user().switchFk(true); // FK 활성화
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Executable -- 프로시저 실행
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const MyDb = defineDbContext({
|
|
312
|
+
tables: { user: User },
|
|
313
|
+
procedures: { getUserOrders: GetUserOrders },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const db = createDbContext(MyDb, executor, { database: "mydb" });
|
|
317
|
+
|
|
318
|
+
await db.connect(async () => {
|
|
319
|
+
const results = await db.getUserOrders().execute({ userId: 1 });
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## expr -- 표현식 빌더
|
|
326
|
+
|
|
327
|
+
### 값/조건
|
|
328
|
+
|
|
329
|
+
| 함수 | 설명 |
|
|
330
|
+
|------|------|
|
|
331
|
+
| `expr.val(type, value)` | 리터럴 값 |
|
|
332
|
+
| `expr.raw(type)\`sql\`` | Raw SQL (태그 템플릿) |
|
|
333
|
+
| `expr.eq(a, b)` | `=` (NULL 안전) |
|
|
334
|
+
| `expr.gt(a, b)` | `>` |
|
|
335
|
+
| `expr.lt(a, b)` | `<` |
|
|
336
|
+
| `expr.gte(a, b)` | `>=` |
|
|
337
|
+
| `expr.lte(a, b)` | `<=` |
|
|
338
|
+
| `expr.between(src, from?, to?)` | BETWEEN |
|
|
339
|
+
| `expr.null(src)` | IS NULL |
|
|
340
|
+
| `expr.like(src, pattern)` | LIKE |
|
|
341
|
+
| `expr.regexp(src, pattern)` | 정규식 매칭 |
|
|
342
|
+
| `expr.in(src, values)` | IN (값 목록) |
|
|
343
|
+
| `expr.inQuery(src, query)` | IN (서브쿼리) |
|
|
344
|
+
| `expr.exists(query)` | EXISTS |
|
|
345
|
+
| `expr.not(cond)` | NOT |
|
|
346
|
+
| `expr.and(conds)` | AND |
|
|
347
|
+
| `expr.or(conds)` | OR |
|
|
348
|
+
|
|
349
|
+
### 문자열 함수
|
|
350
|
+
|
|
351
|
+
| 함수 | 설명 |
|
|
352
|
+
|------|------|
|
|
353
|
+
| `expr.concat(...args)` | 연결 (NULL=빈문자열) |
|
|
354
|
+
| `expr.left(src, len)` | 왼쪽 N자 |
|
|
355
|
+
| `expr.right(src, len)` | 오른쪽 N자 |
|
|
356
|
+
| `expr.substring(src, start, len?)` | 부분 문자열 (1-based) |
|
|
357
|
+
| `expr.trim(src)` | 양쪽 공백 제거 |
|
|
358
|
+
| `expr.padStart(src, len, fill)` | 왼쪽 패딩 (LPAD) |
|
|
359
|
+
| `expr.replace(src, from, to)` | 치환 |
|
|
360
|
+
| `expr.upper(src)` / `expr.lower(src)` | 대/소문자 |
|
|
361
|
+
| `expr.length(src)` | 문자 길이 (CHAR_LENGTH) |
|
|
362
|
+
| `expr.byteLength(src)` | 바이트 길이 (LENGTH/DATALENGTH) |
|
|
363
|
+
| `expr.indexOf(src, search)` | 위치 (1-based, 0=없음) |
|
|
364
|
+
|
|
365
|
+
### 수학 함수
|
|
366
|
+
|
|
367
|
+
| 함수 | 설명 |
|
|
368
|
+
|------|------|
|
|
369
|
+
| `expr.abs(src)` | 절대값 |
|
|
370
|
+
| `expr.round(src, digits)` | 반올림 |
|
|
371
|
+
| `expr.ceil(src)` / `expr.floor(src)` | 올림/내림 |
|
|
372
|
+
|
|
373
|
+
### 날짜 함수
|
|
374
|
+
|
|
375
|
+
| 함수 | 설명 |
|
|
376
|
+
|------|------|
|
|
377
|
+
| `expr.year(src)` / `expr.month(src)` / `expr.day(src)` | 연/월/일 추출 |
|
|
378
|
+
| `expr.hour(src)` / `expr.minute(src)` / `expr.second(src)` | 시/분/초 추출 |
|
|
379
|
+
| `expr.dateDiff(unit, from, to)` | 날짜 차이 |
|
|
380
|
+
| `expr.dateAdd(unit, src, value)` | 날짜 더하기 |
|
|
381
|
+
| `expr.formatDate(src, format)` | 날짜 포맷 (FORMAT/DATE_FORMAT) |
|
|
382
|
+
| `expr.isoWeek(src)` | ISO 주차 |
|
|
383
|
+
| `expr.isoWeekStartDate(src)` | ISO 주 시작일 |
|
|
384
|
+
| `expr.isoYearMonth(src)` | ISO 연월 (YYYYMM 형식) |
|
|
385
|
+
|
|
386
|
+
단위(`DateUnit`): `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`
|
|
387
|
+
|
|
388
|
+
### 집계 함수
|
|
389
|
+
|
|
390
|
+
| 함수 | 설명 |
|
|
391
|
+
|------|------|
|
|
392
|
+
| `expr.count(arg?, distinct?)` | 행 수 |
|
|
393
|
+
| `expr.sum(arg)` | 합계 |
|
|
394
|
+
| `expr.avg(arg)` | 평균 |
|
|
395
|
+
| `expr.max(arg)` / `expr.min(arg)` | 최대/최소 |
|
|
396
|
+
|
|
397
|
+
### 조건 함수
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// CASE WHEN
|
|
401
|
+
expr.switch<string>()
|
|
402
|
+
.case(expr.gt(c.score, 90), "A")
|
|
403
|
+
.case(expr.gt(c.score, 80), "B")
|
|
404
|
+
.default("C")
|
|
405
|
+
|
|
406
|
+
// IF (단순 삼항)
|
|
407
|
+
expr.if(expr.gt(c.score, 60), "pass", "fail")
|
|
408
|
+
|
|
409
|
+
// WHERE -> boolean 컬럼
|
|
410
|
+
expr.is(expr.gt(c.score, 80)) // true/false
|
|
411
|
+
|
|
412
|
+
// NULLIF (source === value이면 NULL 반환)
|
|
413
|
+
expr.nullIf(c.status, "unknown")
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 윈도우 함수
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
// 순위
|
|
420
|
+
expr.rowNumber({ partitionBy: [c.dept], orderBy: [[c.score, "DESC"]] })
|
|
421
|
+
expr.rank({ orderBy: [[c.score, "DESC"]] })
|
|
422
|
+
expr.denseRank({ orderBy: [[c.score, "DESC"]] })
|
|
423
|
+
expr.ntile(4, { orderBy: [[c.score, "DESC"]] })
|
|
424
|
+
|
|
425
|
+
// 탐색
|
|
426
|
+
expr.lag(c.score, 1, 0, { orderBy: [[c.createdAt, "ASC"]] })
|
|
427
|
+
expr.lead(c.score, 1, undefined, { orderBy: [[c.createdAt, "ASC"]] })
|
|
428
|
+
expr.firstValue(c.score, { orderBy: [[c.createdAt, "ASC"]] })
|
|
429
|
+
expr.lastValue(c.score, { orderBy: [[c.createdAt, "ASC"]] })
|
|
430
|
+
|
|
431
|
+
// 윈도우 집계
|
|
432
|
+
expr.sumOver(c.amount, { partitionBy: [c.dept] })
|
|
433
|
+
expr.avgOver(c.amount, { partitionBy: [c.dept] })
|
|
434
|
+
expr.countOver({ partitionBy: [c.dept] })
|
|
435
|
+
expr.minOver(c.amount, { partitionBy: [c.dept] })
|
|
436
|
+
expr.maxOver(c.amount, { partitionBy: [c.dept] })
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### 기타
|
|
440
|
+
|
|
441
|
+
| 함수 | 설명 |
|
|
442
|
+
|------|------|
|
|
443
|
+
| `expr.cast(src, targetType)` | 타입 변환 |
|
|
444
|
+
| `expr.greatest(...args)` | 최대값 |
|
|
445
|
+
| `expr.least(...args)` | 최소값 |
|
|
446
|
+
| `expr.rowNum()` | 행 번호 (전체) |
|
|
447
|
+
| `expr.random()` | 랜덤 0~1 |
|
|
448
|
+
| `expr.subquery(type, queryable)` | 스칼라 서브쿼리 |
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 검색 파서
|
|
453
|
+
|
|
454
|
+
사용자 검색 문법을 SQL LIKE 패턴으로 변환.
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { parseSearchQuery } from "@simplysm/orm-common";
|
|
458
|
+
|
|
459
|
+
parseSearchQuery('apple +fruit -rotten "green apple" wild*');
|
|
460
|
+
// {
|
|
461
|
+
// or: ["%apple%", "wild%"],
|
|
462
|
+
// must: ["%green apple%", "%fruit%"],
|
|
463
|
+
// not: ["%rotten%"],
|
|
464
|
+
// }
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
문법: `term`(OR), `+term`(AND 필수), `-term`(NOT 제외), `"exact"`(정확히 + 필수), `wild*`(접두사)
|
|
468
|
+
|
|
469
|
+
이스케이프: `\\` (리터럴 `\`), `\*` (리터럴 `*`), `\%` (리터럴 `%`), `\"` (리터럴 `"`), `\+` (리터럴 `+`), `\-` (리터럴 `-`)
|
package/docs/schema.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# 스키마 정의
|
|
2
|
+
|
|
3
|
+
## Table
|
|
4
|
+
|
|
5
|
+
불변 빌더 패턴으로 테이블을 정의한다. 모든 메서드는 새 인스턴스를 반환한다.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Table } from "@simplysm/orm-common";
|
|
9
|
+
|
|
10
|
+
const User = Table("user")
|
|
11
|
+
.description("사용자 테이블")
|
|
12
|
+
.database("mydb") // 선택
|
|
13
|
+
.schema("dbo") // MSSQL/PostgreSQL, 선택
|
|
14
|
+
.columns((c) => ({
|
|
15
|
+
id: c.int().autoIncrement(),
|
|
16
|
+
name: c.varchar(100),
|
|
17
|
+
email: c.varchar(200).nullable(),
|
|
18
|
+
role: c.varchar(20).default("user"),
|
|
19
|
+
bio: c.text().nullable(),
|
|
20
|
+
avatar: c.binary().nullable(),
|
|
21
|
+
score: c.decimal(10, 2).nullable(),
|
|
22
|
+
active: c.boolean().default(true),
|
|
23
|
+
birthday: c.date().nullable(),
|
|
24
|
+
loginTime: c.time().nullable(),
|
|
25
|
+
createdAt: c.datetime(),
|
|
26
|
+
externalId: c.uuid().nullable(),
|
|
27
|
+
}))
|
|
28
|
+
.primaryKey("id") // 복합 PK 지원: .primaryKey("col1", "col2")
|
|
29
|
+
.indexes((i) => [
|
|
30
|
+
i.index("email").unique(),
|
|
31
|
+
i.index("name", "role").orderBy("ASC", "DESC"),
|
|
32
|
+
i.index("createdAt").name("ix_created"),
|
|
33
|
+
])
|
|
34
|
+
.relations((r) => ({
|
|
35
|
+
orders: r.foreignKeyTarget(() => Order, "user"), // 1:N
|
|
36
|
+
profile: r.foreignKeyTarget(() => Profile, "user").single(), // 1:1
|
|
37
|
+
}));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 컬럼 타입
|
|
41
|
+
|
|
42
|
+
| 메서드 | SQL 타입 | TypeScript 타입 |
|
|
43
|
+
|--------|---------|----------------|
|
|
44
|
+
| `c.int()` | INT | `number` |
|
|
45
|
+
| `c.bigint()` | BIGINT | `number` |
|
|
46
|
+
| `c.float()` | FLOAT | `number` |
|
|
47
|
+
| `c.double()` | DOUBLE | `number` |
|
|
48
|
+
| `c.decimal(p, s?)` | DECIMAL(p,s) | `number` |
|
|
49
|
+
| `c.varchar(len)` | VARCHAR(len) | `string` |
|
|
50
|
+
| `c.char(len)` | CHAR(len) | `string` |
|
|
51
|
+
| `c.text()` | LONGTEXT/TEXT | `string` |
|
|
52
|
+
| `c.boolean()` | BIT/TINYINT(1) | `boolean` |
|
|
53
|
+
| `c.datetime()` | DATETIME | `DateTime` |
|
|
54
|
+
| `c.date()` | DATE | `DateOnly` |
|
|
55
|
+
| `c.time()` | TIME | `Time` |
|
|
56
|
+
| `c.binary()` | LONGBLOB/VARBINARY/BYTEA | `Bytes` |
|
|
57
|
+
| `c.uuid()` | UNIQUEIDENTIFIER/UUID | `Uuid` |
|
|
58
|
+
|
|
59
|
+
### 컬럼 수정자
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
c.int().autoIncrement() // 자동 증가 (INSERT 시 선택적)
|
|
63
|
+
c.varchar(100).nullable() // NULL 허용 (타입에 undefined 추가)
|
|
64
|
+
c.varchar(20).default("x") // 기본값 (INSERT 시 선택적)
|
|
65
|
+
c.varchar(100).description("설명")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 타입 추론
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
User.$inferSelect // { id: number; name: string; email: string | undefined; ... }
|
|
72
|
+
User.$inferInsert // { name: string; createdAt: DateTime; } & { id?: number; email?: string; ... }
|
|
73
|
+
User.$inferUpdate // { id?: number; name?: string; ... } (모든 필드 선택적)
|
|
74
|
+
User.$inferColumns // { id: number; name: string; email: string | undefined; ... } (관계 제외)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 관계 (Relation)
|
|
80
|
+
|
|
81
|
+
### foreignKey -- N:1 (DB FK 생성)
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const Order = Table("order")
|
|
85
|
+
.columns((c) => ({
|
|
86
|
+
id: c.int().autoIncrement(),
|
|
87
|
+
userId: c.int(),
|
|
88
|
+
}))
|
|
89
|
+
.primaryKey("id")
|
|
90
|
+
.relations((r) => ({
|
|
91
|
+
user: r.foreignKey(["userId"], () => User), // Order.userId -> User.id
|
|
92
|
+
}));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### foreignKeyTarget -- 1:N / 1:1 (역참조)
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const User = Table("user")
|
|
99
|
+
.columns((c) => ({ id: c.int().autoIncrement(), name: c.varchar(100) }))
|
|
100
|
+
.primaryKey("id")
|
|
101
|
+
.relations((r) => ({
|
|
102
|
+
orders: r.foreignKeyTarget(() => Order, "user"), // 1:N (배열)
|
|
103
|
+
profile: r.foreignKeyTarget(() => Profile, "user").single(), // 1:1 (단일 객체)
|
|
104
|
+
}));
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### relationKey / relationKeyTarget -- 논리적 관계 (DB FK 없음)
|
|
108
|
+
|
|
109
|
+
View에서도 사용 가능. DB에 FK 제약조건을 생성하지 않는다.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
.relations((r) => ({
|
|
113
|
+
category: r.relationKey(["categoryId"], () => Category),
|
|
114
|
+
items: r.relationKeyTarget(() => Item, "parent"),
|
|
115
|
+
}))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## View
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { View, expr } from "@simplysm/orm-common";
|
|
124
|
+
|
|
125
|
+
const UserSummary = View("user_summary")
|
|
126
|
+
.database("mydb")
|
|
127
|
+
.query<typeof MyDb>((db) =>
|
|
128
|
+
db.user()
|
|
129
|
+
.select((c) => ({
|
|
130
|
+
userId: c.id,
|
|
131
|
+
userName: c.name,
|
|
132
|
+
orderCount: expr.count(),
|
|
133
|
+
totalAmount: expr.sum(c.amount),
|
|
134
|
+
}))
|
|
135
|
+
.groupBy((c) => [c.id, c.name])
|
|
136
|
+
)
|
|
137
|
+
.relations((r) => ({
|
|
138
|
+
user: r.relationKey(["userId"], () => User),
|
|
139
|
+
}));
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**DbContext 등록:**
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const MyDb = defineDbContext({
|
|
146
|
+
tables: { user: User },
|
|
147
|
+
views: { userSummary: UserSummary },
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Procedure
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { Procedure } from "@simplysm/orm-common";
|
|
157
|
+
|
|
158
|
+
const GetUserOrders = Procedure("get_user_orders")
|
|
159
|
+
.database("mydb")
|
|
160
|
+
.params((c) => ({
|
|
161
|
+
userId: c.int(),
|
|
162
|
+
fromDate: c.date().nullable(),
|
|
163
|
+
}))
|
|
164
|
+
.returns((c) => ({
|
|
165
|
+
orderId: c.int(),
|
|
166
|
+
amount: c.decimal(10, 2),
|
|
167
|
+
createdAt: c.datetime(),
|
|
168
|
+
}))
|
|
169
|
+
.body(`
|
|
170
|
+
SELECT id AS orderId, amount, created_at AS createdAt
|
|
171
|
+
FROM orders
|
|
172
|
+
WHERE user_id = userId AND created_at >= COALESCE(fromDate, '1900-01-01')
|
|
173
|
+
`);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**DbContext 등록:**
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const MyDb = defineDbContext({
|
|
180
|
+
tables: { user: User },
|
|
181
|
+
procedures: { getUserOrders: GetUserOrders },
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
MSSQL은 파라미터에 `@` 접두사 필요: `@userId`, `@fromDate`
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Index
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
.indexes((i) => [
|
|
193
|
+
i.index("email").unique(), // UNIQUE INDEX
|
|
194
|
+
i.index("name", "role").orderBy("ASC", "DESC"), // 정렬 방향 지정
|
|
195
|
+
i.index("createdAt").name("ix_custom_name"), // 커스텀 이름
|
|
196
|
+
i.index("code").description("코드 인덱스"),
|
|
197
|
+
])
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## defineDbContext / createDbContext
|
|
203
|
+
|
|
204
|
+
### defineDbContext -- 스키마 블루프린트
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { defineDbContext } from "@simplysm/orm-common";
|
|
208
|
+
|
|
209
|
+
const MyDb = defineDbContext({
|
|
210
|
+
tables: { user: User, order: Order },
|
|
211
|
+
views: { userSummary: UserSummary },
|
|
212
|
+
procedures: { getUserOrders: GetUserOrders },
|
|
213
|
+
migrations: [
|
|
214
|
+
{
|
|
215
|
+
name: "20260105_001_add_phone",
|
|
216
|
+
up: async (db) => {
|
|
217
|
+
const c = createColumnFactory();
|
|
218
|
+
await db.addColumn({ name: "user" }, "phone", c.varchar(20).nullable());
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`_migration` 테이블이 자동으로 포함된다.
|
|
226
|
+
|
|
227
|
+
### createDbContext -- 런타임 인스턴스 생성
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { createDbContext } from "@simplysm/orm-common";
|
|
231
|
+
|
|
232
|
+
const db = createDbContext(MyDb, executor, {
|
|
233
|
+
database: "mydb",
|
|
234
|
+
schema: "dbo", // MSSQL/PostgreSQL 선택
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`executor`는 `DbContextExecutor` 인터페이스를 구현해야 한다 (`@simplysm/orm-node`의 `NodeDbContextExecutor` 등).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/orm-common",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.86",
|
|
4
4
|
"description": "Simplysm Package - ORM Module (common)",
|
|
5
5
|
"author": "simplysm",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -20,6 +20,6 @@
|
|
|
20
20
|
],
|
|
21
21
|
"sideEffects": false,
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@simplysm/core-common": "13.0.
|
|
23
|
+
"@simplysm/core-common": "13.0.86"
|
|
24
24
|
}
|
|
25
25
|
}
|