Files
five/docs/RTL-Go-Native-Migration.md
CharlesKWON 2d82541d3d docs: Phase C contrib TODO + Phase A/B completion marker
Create Five-1.0-Phase-C-TODO.md capturing the remaining 1.0 work:
three Harbour contrib libraries (hbct Clipper Tools, hbnf Numeric
Functions, hbtip TCP/IP/SMTP/POP3/HTTP). Each entry lists the
Harbour source path, a minimum first-pass scope, and an effort
estimate. Suggested order: hbct → hbtip → hbnf. Total ~6-10 days.

Update RTL-Go-Native-Migration.md "남은 병목" with the Phase A/B
completion list — six features shipped this session — plus a note
that the 11 HBTYPE functions the initial analysis flagged are
actually Harbour's internal scalar class factories, not user-facing
blockers (Five's SendBuiltin covers the same surface).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:38:08 +09:00

1036 lines
65 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RTL Go-Native 전환 계획
PRG 핫패스와 `hbrtl/` RTL 함수 중 Go 네이티브 구현이 이익인 후보 목록 및 진행 기록. 기준선: 최종 결과가 **Harbour와 동일**해야 한다.
## 배경
FiveSql2 성능 개선 흐름(`3caadb2 SqlOrderBy+SqlGroupBy Go RTL`, `5fc9c3b SqlHashJoin Go RTL` 등)은 PRG 핫루프를 Go RTL로 옮겨 큰 이득을 보였다. 본 문서는 같은 패턴을 체계적으로 적용할 후보를 추린다.
## Harbour 호환 검증 근거
| 항목 | 근거 | 영향 |
|------|------|------|
| 해시 기본 플래그 | [harbour-core/include/hbapi.h:927-931](../harbour-core/include/hbapi.h#L927-L931) — `HB_HASH_FLAG_DEFAULT = HB_HASH_AUTOADD_ASSIGN \| HB_HASH_BINARY \| HB_HASH_KEEPORDER` | 삽입 순서 보존 + `memcmp` 정확 비교 |
| 해시 키 비교 | [harbour-core/src/vm/hashes.c:167-182](../harbour-core/src/vm/hashes.c#L167-L182) — `hb_hashItemCmp` | CHAR padding trim 없음, Date/Timestamp는 julian 비교 |
| 내부 탐색 | `pPairs[]` + `pnPos[]` 이진 탐색 (O(log N)) | Five의 Go map 치환은 O(1)로 상회 |
## 후보 목록
### ✅ Tier 1 — 즉시 이익, 시맨틱 안전
| # | 대상 | 파일 | 방식 | 예상 효과 | 상태 |
|---|------|------|------|-----------|------|
| 1 | Hash 스토리지 | [hbrtl/hash.go](../hbrtl/hash.go), [hbrt/ops_collection.go](../hbrt/ops_collection.go), [hbrt/value.go](../hbrt/value.go) | `map[string]int` 인덱스 추가, 삽입 순서 슬라이스 유지 | 50100x | **완료 (2026-04-17)** |
| 2 | SqlDistinct | [_FiveSql2/src/TSqlSort.prg:57-70](../_FiveSql2/src/TSqlSort.prg#L57-L70), [hbrtl/sqlscan.go](../hbrtl/sqlscan.go) | Go RTL `map[string]struct{}` + `strings.Builder` | 100300x | **완료 (2026-04-17)** |
| 3 | SqlRowCompare NULL · 혼합타입 정합성 | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlSort.prg](../_FiveSql2/src/TSqlSort.prg), [_FiveSql2/src/TSqlExecutor.prg](../_FiveSql2/src/TSqlExecutor.prg) | Go/PRG 양 경로 NULL 순서 PRG 시맨틱으로 통일 + `NULLS FIRST/LAST` 배선 | 정합성 수정 | **완료 (2026-04-17)** |
### ✅ Tier 2 — 블록 NIL 특화 + 누락 타입 보강 (완료)
| # | 대상 | 파일 | 방식 | 상태 |
|---|------|------|------|------|
| 4 | ASort 타입 특화 + 정확성 | [hbrtl/array.go:134-300](../hbrtl/array.go#L134-L300) | 비교자 블록 없을 때 1회 타입 스캔 → 특화 비교자. Date/Logical/Timestamp 지원 추가 (기존엔 no-op) | **완료 (2026-04-17)** |
| 5 | AScan fast-path | [hbrtl/array.go:302-380](../hbrtl/array.go#L302-L380) | 검색값이 string/int/double일 때 타입별 인라인 루프. 드물게 쓰는 타입은 `valuesEqual` fallback | **완료 (2026-04-17)** |
### 🔎 Tier 3 — 내부 헬퍼 최적화
| # | 대상 | 파일 | 방식 | 상태 |
|---|------|------|------|------|
| 6 | RAT 역방향 스캔 | [hbrtl/strings2.go:16-51](../hbrtl/strings2.go#L16-L51) | 검토 결과 `strings.LastIndex` + 부분슬라이스는 이미 최적. 변경 없음 | **검토 종료 (2026-04-17)** |
| 7 | SqlExprHasAgg | [hbrtl/sqlexpr.go](../hbrtl/sqlexpr.go) | PRG 재귀 → Go AST walker + 상수 시간 agg 이름 조회 | **완료 (2026-04-17)** |
### ✅ Tier 4 — DML Boundary-Crossing 감소 (완료)
| # | 대상 | 파일 | 방식 | 상태 |
|---|------|------|------|------|
| 8 | SqlBulkInsert | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg](../_FiveSql2/src/TSqlExecutor.prg) | CTE/subquery/tmp 테이블 materialize 경로의 `FOR j ... dbAppend ... FOR k ... FieldPut` 이중 루프를 Go RTL 단일 호출로 대체 | **완료 (2026-04-17)** |
| 9 | SqlBulkUpdate | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg RunUpdate](../_FiveSql2/src/TSqlExecutor.prg) | UPDATE 스캔 루프 전체를 Go RTL로 이관. WHERE + SET 값 표현식을 pcode로 컴파일해 PRG 메서드 디스패치 제거 | **완료 (2026-04-17)** |
| 10 | MEMRDD 자동 임포트 | [compiler/gengo/gengo.go](../compiler/gengo/gengo.go) | 모든 Five 프로그램에 `_ "five/hbrdd/mem"` 블랭크 임포트 자동 추가 → `USE "mem:x" VIA "MEMRDD"` 즉시 사용 가능 | **완료 (2026-04-17)** |
| 11 | PcCompile 결과 캐시 | [hbrtl/pcexpr.go](../hbrtl/pcexpr.go) | `sync.Map`으로 소스 문자열 키 캐시. 반복 쿼리에서 파서+genpc 건너뛰기 | **완료 (2026-04-17)** |
| 12 | SQL 플랜 캐시 + HbDeepClone | [_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg), [hbrtl/array.go](../hbrtl/array.go) | `cSQL → hQuery` PRG 해시 캐시. 히트 시 Go RTL `HbDeepClone`으로 pristine 사본 반환 → `SqlFoldConst` 인-플레이스 변경 안전 | **완료 (2026-04-17)** |
| 13 | 파라미터 바인딩 벤치 입증 | [_FiveSql2/test/bench_prep_sql.prg](../_FiveSql2/test/bench_prep_sql.prg) | 기존 `five_SQL(cSQL, aParams)` + `?` 파서가 이미 지원. 플랜 캐시와 결합 시 SELECT 1.58x, INSERT 1.12x | **입증 (2026-04-17)** |
| 14 | CTE → MEMRDD | [_FiveSql2/src/TSqlExecutor.prg](../_FiveSql2/src/TSqlExecutor.prg), [hbrdd/mem/memrdd.go](../hbrdd/mem/memrdd.go), [hbrtl/sqlscan.go SqlBulkInsert](../hbrtl/sqlscan.go) | 3곳 materialize 경로를 `dbCreate("mem:xxx", ..., "MEMRDD")` + MEMRDD `dbUseArea`로 전환. SqlBulkInsert가 `*dbf.DBFArea` 외 일반 `hbrdd.Area`도 처리하도록 확장. MEMRDD Create가 필드명 trailing-space trim | **완료 (2026-04-17)** |
| 15 | SqlWindowPartitions Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg) | PARTITION BY 키 빌드 + 행-인덱스 그룹핑을 Go RTL에 위임. N·M 경계 크로싱 → 1 | **완료 (2026-04-17)** |
| 16 | SqlWindowSortPartition Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg) | 파티션 내 ORDER BY를 Go `sort.SliceStable` + 사전 해석된 컬럼 인덱스로 처리. PRG 비교 블록 제거 | **완료 (2026-04-17)** |
| 17 | SqlGroupRows Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlAgg.prg GroupBy](../_FiveSql2/src/TSqlAgg.prg) | GROUP BY 그룹 빌드 루프만 Go RTL로. 집계·HAVING은 복잡 표현식 대응 위해 PRG 유지 | **완료 (2026-04-17)** |
| 18 | SqlComputeAggSimple Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlAgg.prg ComputeAgg](../_FiveSql2/src/TSqlAgg.prg) | COUNT/SUM/AVG/MIN/MAX + 컬럼 인자 fast-path. 복잡 인자·GROUP_CONCAT은 PRG fallback | **완료 (2026-04-17)** |
| 19 | SQL 스칼라 헬퍼 Go RTL | [hbrtl/sqlhelpers.go](../hbrtl/sqlhelpers.go), [_FiveSql2/src/TSqlFunc.prg](../_FiveSql2/src/TSqlFunc.prg) | `SqlIsTrue/SqlCmpEq/SqlCmpLt/SqlCoerceForCmp/SqlCoerceNum/SqlCoerceStr` 6개 Go로. PRG tree-walker 평가 경로(HAVING, complex expr) 오버헤드 감소 | **완료 (2026-04-17)** |
| 20 | SQL 템플릿 자동 파라미터화 | [hbrtl/sqlhelpers.go SqlExtractTemplate](../hbrtl/sqlhelpers.go), [_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg) | 리터럴(`TK_TEXT`/`TK_NUM`)을 `TK_QMARK`로 치환 + 템플릿 키로 플랜 캐시. 동일 구조 다른 값 쿼리가 캐시 공유 | **완료 (2026-04-17)** |
| 21 | TSqlLexer Go 포팅 + 결합 | [hbrtl/sqlhelpers.go SqlLexerTokenize + SqlLexAndExtractTemplate](../hbrtl/sqlhelpers.go), [_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg) | PRG `SubStr` 기반 문자-단위 렉서를 Go byte-level FSM으로. 자동-파라미터화와 결합해 1회 Go 호출로 lex+normalize 완료 | **완료 (2026-04-17)** |
| 22 | SqlWindowAssignRank Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlExecutor.prg ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg) | ROW_NUMBER/RANK/DENSE_RANK 배정 루프를 Go에서. 파티션당 1회 호출로 per-row SqlWinRowsEqual PRG 호출 제거 | **완료 (2026-04-17)** |
| 23 | HbDeepClone 성능 개선 | [hbrtl/array.go deepCloneValue](../hbrtl/array.go) | 스칼라 원소는 재귀 스킵 (슬롯 복사만), 해시 키 공유 (문자열/숫자는 불변). 플랜 캐시 히트마다 수행되는 핫패스 | **완료 (2026-04-17)** |
| 24 | WA 캐시 + 지연 commit | [hbrtl/sqlwacache.go](../hbrtl/sqlwacache.go), [_FiveSql2/src/TSqlExecutor.prg SqlExecOpenTable/CloseTable + RunInsert/Update/Delete](../_FiveSql2/src/TSqlExecutor.prg) | 워크에어리어 공정-수명 캐시 (opt-in). 활성화 시 DML의 per-query dbUseArea/dbCloseArea/dbCommit 전부 배치 → **B12 INSERT 48x** | **완료 (2026-04-17)** |
| 25 | Plan pcode 캐시 + SqlBulkUpdate flush 지연 | [_FiveSql2/src/TSqlExecutor.prg s_hDmlPcodeCache + cCacheKey](../_FiveSql2/src/TSqlExecutor.prg), [hbrtl/sqlscan.go SqlBulkUpdate](../hbrtl/sqlscan.go) | 플랜 키별 컴파일된 pcode(aFPos/where/set_pc) 캐시 + WA cache 활성 시 Go RTL 내부 `Flush()` 스킵 → **B13 UPDATE 48x** | **완료 (2026-04-17)** |
| 26 | SELECT 경로 plan pcode 캐시 | [_FiveSql2/src/TSqlExecutor.prg RunSelect fast path](../_FiveSql2/src/TSqlExecutor.prg) | #25의 패턴을 SELECT fast-path에도 적용. `TryBuildFieldPositions` + `TryCompileWhere` 결과를 `cCacheKey#sel`로 캐시. 반복 SELECT의 PRG AST walk 제거 | **완료 (2026-04-17)** |
| 27 | SqlEvalHaving Go RTL | [hbrtl/sqlscan.go](../hbrtl/sqlscan.go), [_FiveSql2/src/TSqlAgg.prg EvalHaving](../_FiveSql2/src/TSqlAgg.prg) | HAVING 트리 walker를 Go로. ND_LIT/ND_NIL/ND_COL/ND_FN(5 aggs)/ND_BIN/ND_UNI 처리. 복잡 케이스는 PRG fallback | **완료 (2026-04-17, 효과 미미)** |
| 28 | VM Function() + Symbol 캐시 | [hbrt/call.go Function](../hbrt/call.go), [hbrt/vm.go GetSym](../hbrt/vm.go), [hbrt/thread.go pushPendingSym](../hbrt/thread.go), [compiler/gengo/gengo.go emitPushSymbol](../compiler/gengo/gengo.go) | 모든 PRG 함수 호출이 통과하는 경로. (a) Function()의 pop-push dance를 stack shift로 (heap alloc + 2N+2 ops → 1 copy), (b) 심볼 resolve 결과를 sym.Func에 캐시, (c) gengo가 심볼 포인터를 package-level var에 hoist해 `FindSymbol` 호출/RWMutex/map lookup을 lazy 1회로 단축. 전역 3-15% 개선 | **완료 (2026-04-17)** |
### ❌ 제외 (Harbour 호환 리스크 과다)
| 대상 | 제외 이유 |
|------|----------|
| SqlLikeMatch `regexp` 치환 | Harbour SQL LIKE의 `%`/`_`/`[abc]`/`[!abc]`/이스케이프 규칙은 regex와 미스매치. 자체 매처 필요 |
| SubStr Go slice 직접 | 이미 slice 사용 중 ([hbrtl/strings.go:149](../hbrtl/strings.go#L149)). 변경 이익 없음 |
| Descend `bytes.Map` | 성능 이익 <5% |
| SET DATE 비트셋 사전계산 | 515%지만 `setDateFormat` 전역 일관성 리스크 > 이익 |
### ✅ 이미 최적 (건드리지 말 것)
- [hbrtl/crypto.go](../hbrtl/crypto.go) MD5/SHA256/BASE64/CRC32 — `crypto/md5`, `crypto/sha256`, `encoding/base64` 사용 중
- [hbrtl/binconv.go](../hbrtl/binconv.go) BIN2I/L/W — `encoding/binary` 사용 중
- [hbrtl/regex.go](../hbrtl/regex.go) — `regexp` 사용 중
## 진행 기록
### #1 Hash 스토리지 O(1) 전환 — 2026-04-17 완료
**구조 변경** ([hbrt/value.go:237-249](../hbrt/value.go#L237-L249))
```go
type HbHash struct {
Keys []Value // 삽입 순서 (HB_HASH_KEEPORDER 기본)
Values []Value // 병렬
Order []int
Flags int32
Index map[string]int // 신규: O(1) 탐색용 미러
}
```
**신규 파일**: [hbrt/hash_helpers.go](../hbrt/hash_helpers.go)
- `hashKey(v Value) (string, bool)``valueEqual` 동치류와 일치하는 직렬화. Nil/String/Numeric/Logical/Date/Timestamp 지원. 수치는 정수로 환산 가능하면 `'I'` 폼으로 정규화 (int/double 교차 매칭), -0.0 → +0.0
- `(*HbHash).Lookup/Has/Set/Append/Delete/HashGet/ensureIndex/HashFromPairs` 메서드
- 비인덱싱 키 타입(Array/Hash/Block/Pointer)은 fallback 선형 스캔 + `valueEqual`
**호출부 전환**
| 파일 | 변경 |
|------|------|
| [hbrt/ops_collection.go](../hbrt/ops_collection.go) | `HashGen`/`ArrayPush`/`ArrayPop` 헬퍼 경유. `HashGen`은 pair 수집 후 `Set`로 last-wins 보장 |
| [hbrt/valuemethods.go](../hbrt/valuemethods.go) | `vmHashHas`/`vmHashDelete` 헬퍼 경유 |
| [hbrt/hbfunc.go](../hbrt/hbfunc.go) | `HashAdd``Set`, `HashGetC``"S"+key` 직접 Index 힛 |
| [hbrt/macroeval.go](../hbrt/macroeval.go) | 해시 리터럴 평가 `Set` (중복 키 last-wins) |
| [hbrt/gobridge.go](../hbrt/gobridge.go) | `reflect.Map` 변환 `Append` (Go map은 중복 키 없음) |
| [hbrtl/hash.go](../hbrtl/hash.go) | 7개 RTL 함수 (HbHash/HGet/HSet/HDel/HHasKey/HKeys/HValues) 전체 헬퍼 경유 |
| [hbrtl/json.go](../hbrtl/json.go) | `navigatePath`/`JsonMerge` 헬퍼 경유 |
**Harbour 호환 보장**
- 키 삽입 순서 보존 (`hb_HKeys()` 반환): `Keys[]` 슬라이스 유지
- `HB_HASH_BINARY` 정확 비교: `hashKey`가 String을 raw bytes로 직렬화
- 수치 교차 비교 (`1 == 1.0`): 정수로 환산 가능한 double은 `'I'` 폼으로 정규화
- 비인덱싱 키: `valueEqual` fallback (Array/Hash/Block 포인터 동일성 포함)
**스테일 방지**: `ensureIndex()`는 Index가 nil이거나 indexable 키 개수와 불일치하면 재구축. 테스트가 `.Keys = append(...)`로 직접 조작해도 다음 Lookup 시점에 자동 복구.
**검증 (CLAUDE.md 3종)**
- `go test ./...` — 15 패키지 ALL PASS
- FiveSql2 — 43/43 (100%)
- Harbour compat — 51/51 (100%)
### #2 SqlDistinct Go RTL — 2026-04-17 완료
**추가 함수** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `appendValueHashKey(sb *strings.Builder, v)``valueHashKey`와 동일 매핑이나 중간 문자열 할당 없이 Builder에 직접 기록
- `SqlDistinct(aRows) → aRows` — Go map 기반 단일 패스 dedup, 입력 순서 보존
**호출부 변경** ([_FiveSql2/src/TSqlSort.prg:57-62](../_FiveSql2/src/TSqlSort.prg#L57-L62))
```harbour
METHOD Distinct( aRows ) CLASS TSqlSort
RETURN SqlDistinct( aRows )
```
PRG의 `hb_HHasKey` 루프 + 수동 `cKey += SqlValToStr(..) + "|"` 조립을 Go 한 번 호출로 대체. 컴파일러의 "undeclared variable" 경고는 RTL 함수 심볼이 gengo 테이블에 없기 때문 — 런타임에 `SQLDISTINCT` 심볼로 해결되어 동작은 정상.
**Harbour 호환 보장**
- 키 구성 규칙이 `SqlValToStr` 시맨틱(`appendValueHashKey`)과 byte-for-byte 일치 — CHAR는 trailing space trim, NIL은 `\x00NIL`, 숫자는 int/double 별도 경로
- 입력 순서 보존 → SQL DISTINCT 결과의 첫 등장 순서 유지
- 빈 배열 · 단일 행은 입력 그대로 반환 (PRG 동작과 일치)
**등록** ([hbrtl/register.go:626](../hbrtl/register.go#L626))
```go
hbrt.Sym("SQLDISTINCT", hbrt.FsPublic, SqlDistinct),
```
**검증**
- `go test ./...` — ALL PASS
- FiveSql2 — 43/43 (100%)
- Harbour compat — 51/51 (100%)
### #3 SqlRowCompare NULL 순서 · 혼합타입 정합성 — 2026-04-17 완료
**발견된 문제**
1. Go `SqlOrderBy` 기본값이 NIL을 가장 작은 값으로 취급 (ASC에서 NULLs FIRST) — PRG `SqlRowCompare`의 원래 시맨틱(NIL = 가장 큼)과 정반대
2. 파서가 `NULLS FIRST/LAST` (SQL:2003) 스펙을 파싱하지만 ([TSqlParser2.prg:962-973](../_FiveSql2/src/TSqlParser2.prg#L962-L973)) Go/PRG 어느 경로도 이를 읽지 않음 — 명시 스펙이 완전 무시
3. Go `compareValues`가 숫자 vs 문자열 혼합 타입 비교를 지원하지 않음 — PRG는 `Val(AllTrim(x))`로 강제변환 ([TSqlSort.prg:145-148](../_FiveSql2/src/TSqlSort.prg#L145-L148))
**수정 내역**
| 파일 | 변경 |
|------|------|
| [hbrtl/sqlscan.go](../hbrtl/sqlscan.go) | `sortCol``nullsFirst bool` 필드 추가. `cDir == DESC`를 기본값으로 하고 `arr.Items[2]``"FIRST"`/`"LAST"`면 오버라이드. `compareValues``compareValuesNonNil` 기반으로 재구성하고 NIL 처리를 호출부로 이관. 혼합 N/C 비교용 `parseLeadingNumeric` 추가 |
| [_FiveSql2/src/TSqlExecutor.prg:3818](../_FiveSql2/src/TSqlExecutor.prg#L3818) | `TryBuildSortSpec``aOrderBy[i][3]`을 읽어 3번째 요소 `cNulls`로 Go 스펙에 전달 |
| [_FiveSql2/src/TSqlSort.prg:33-54](../_FiveSql2/src/TSqlSort.prg#L33-L54) | `OrderBy` 메서드가 `aOB[i][3]``s_aOBCols`에 보존 |
| [_FiveSql2/src/TSqlSort.prg:118-144](../_FiveSql2/src/TSqlSort.prg#L118-L144) | `SqlRowCompare`가 명시 `NULLS FIRST/LAST`를 우선 적용, 없으면 `cDir == DESC`를 기본 |
**Harbour/FiveSql2 시맨틱 보장**
- 기본값: NIL은 가장 큰 값 → ASC는 NULLs LAST, DESC는 NULLs FIRST (PRG 원래 동작, PostgreSQL 기본과 일치)
- `NULLS FIRST/LAST` 명시 시 방향과 무관하게 스펙 우선
- 혼합 N/C 비교: PRG `Val(AllTrim(x))` 동작 복제 (선행 공백 무시, 부호/소수점 허용)
**회귀 테스트** — [_FiveSql2/test/test_null_order.prg](../_FiveSql2/test/test_null_order.prg)
4/4 PASS: default ASC, default DESC, ASC NULLS FIRST, DESC NULLS LAST
**검증**
- `go test ./...` — ALL PASS
- FiveSql2 43/43 · Harbour compat 51/51
### #4, #5 ASort 정확성/특화 + AScan fast-path — 2026-04-17 완료
**ASort 버그 발견** — 기본 비교 경로([기존 array.go:164](../hbrtl/array.go#L164))가 `IsString` / `IsNumeric`가 아닌 타입에 대해 `return false`를 반환 → 날짜·논리값·타임스탬프 배열 정렬이 **no-op**.
**수정 내역** ([hbrtl/array.go](../hbrtl/array.go))
- `detectArrayKind(items)` — 1회 스캔으로 동종 배열 분류 (Int / Numeric / String / Date / Timestamp / Logical / Mixed)
- 분류 결과에 따라 타입 특화 `sort.SliceStable` 선택. Int 배열은 `AsNumInt`만 써서 double 변환 생략
- Mixed는 `valueLess` fallback — Harbour `<` 시맨틱 (NIL 가장 작음, 타입 내 비교)
**AScan fast-path** ([hbrtl/array.go:302-380](../hbrtl/array.go#L302-L380))
- 검색값이 문자열·정수·실수일 때 타입별 인라인 루프 — `valuesEqual` 호출·switch·타입 체크 생략
- 정수 검색 + 배열 내 double 원소는 cross-type 비교 (`item.AsNumDouble() == float64(n)`) — 기존 `valuesEqual` 시맨틱 그대로
- Date/Timestamp/Logical/NIL 검색은 `valuesEqual` fallback
**회귀 테스트** ([tests/compat_harbour.prg:328-349](../tests/compat_harbour.prg#L328-L349))
- `9c1 ASort dates ascending` — julian 기준 정렬 (신규)
- `9c2 ASort logicals: F,F,T,T` — 논리값 정렬 (신규)
- `9e1 AScan int found` — 정수 탐색 (신규)
- `9e2 AScan int cross-type` — 정수로 저장된 배열에 double 검색 (신규)
- `9e3 AScan int not found` — 부재 케이스 (신규)
**Harbour 호환 보장**
- 블록이 주어지면 100% 기존 동작 유지 (부작용 보존)
- 블록 NIL 경로는 Harbour 기본 `<` 시맨틱 복제. 이전엔 깨져 있던 날짜/논리값이 이제 올바르게 정렬
**검증**
- `go test ./...` — ALL PASS
- FiveSql2 — 43/43
- Harbour compat — **56/56** (51 기존 + 5 신규)
### #6 RAT 재검토 — 2026-04-17 종결(변경 없음)
`strings.LastIndex`는 Boyer-Moore/Rabin-Karp 최적화 내장. `target[:from]` 슬라이스는 Go에서 O(1)·할당-프리. nOccurrence=1 경로(실제 대부분)는 이미 단일 `LastIndex` 호출. >1 경로는 이전 매치 위치에서 시작해 누적 O(n)이므로 수동 역방향 스캔 대비 이득 없음. **원본 유지**.
### #7 SqlExprHasAgg Go walker — 2026-04-17 완료
**동기**: 매 쿼리마다 SELECT 컬럼 표현식 × 재귀 깊이 만큼 호출 — 깊은 식에서 PRG VM 프레임 셋업 비용이 누적됨. 호출 지점: [TSqlAgg.prg:41,62,85,134](../_FiveSql2/src/TSqlAgg.prg), [TSqlExecutor.prg:1298](../_FiveSql2/src/TSqlExecutor.prg#L1298) 등 6곳.
**구현** ([hbrtl/sqlexpr.go](../hbrtl/sqlexpr.go))
- `aggFuncSet``map[string]struct{}` (상수시간 룩업). AGG_FUNCTIONS 매크로와 완전 일치 (COUNT/SUM/AVG/MIN/MAX/GROUP_CONCAT/STRING_AGG/LISTAGG/JSON_ARRAYAGG/JSON_OBJECTAGG/XMLAGG/ANY_VALUE/BOOL_AND/BOOL_OR)
- `sqlExprHasAggWalk` — PRG SqlExprHasAgg와 **byte-for-byte 동일한 재귀 트리 순회**. ND_FN/ND_BIN/ND_UNI/ND_CASE 가지 커버. ND_WINDOW/ND_SUB 의도적 미순회 (각자 집계 스코프 보유)
- 상수 `ndLit`, `ndCol`, `ndFn` 등 — `FiveSqlDef.ch`의 kind 번호와 동일
**호출부 변경**
- [_FiveSql2/src/TSqlExpr.prg:45-49](../_FiveSql2/src/TSqlExpr.prg#L45-L49) — PRG `FUNCTION SqlExprHasAgg` 제거 (심볼 충돌 방지). 주석으로 Go RTL 위임 명시
- [hbrtl/register.go](../hbrtl/register.go) — `SQLEXPRHASAGG` 공개 심볼 등록
- 기존 호출부(`SqlExprHasAgg(xE)`) 그대로 동작 — RTL 심볼이 해결
**Harbour 호환 보장**: AST kind 번호가 PRG와 정확히 일치. agg 함수 이름 집합이 `AGG_FUNCTIONS` 매크로와 정확히 일치. 재귀 가지 로직이 PRG와 줄 단위로 매치 (`IF xE[1] == ND_FN .AND. SqlIsAggName(xE[2])` 등).
**검증**
- `go test ./...` — ALL PASS
- FiveSql2 — 43/43
- Harbour compat — 56/56
### #8 SqlBulkInsert Go RTL — 2026-04-17 완료
**동기**: `dbAppend`/`FieldPut`은 이미 Go RTL. 병목은 **PRG 루프가 행·컬럼 단위로 Go RTL을 호출하는 boundary crossing**. N행 × M컬럼 = N·M 회 VM 프레임 셋업 + 스택 push/pop + 파라미터 마샬링.
**구현** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `SqlBulkInsert(aRows) → nInserted` — 현재 workarea의 `*DBFArea`에 직접 `Append()` + `PutValue()` + `Flush()`
- NIL 원소는 필드 건너뜀 (PRG `IF aRows[j][k] != NIL` 보존)
- 행 길이가 필드 수 초과 시 초과분 무시, 부족 시 나머지 필드는 default
**호출부 치환** — 동일 형상 루프 3곳 → 1줄
| 위치 | 맥락 |
|------|------|
| [TSqlExecutor.prg:2310](../_FiveSql2/src/TSqlExecutor.prg#L2310) | CREATE TABLE AS SELECT / 임시테이블 로드 |
| [TSqlExecutor.prg:2630](../_FiveSql2/src/TSqlExecutor.prg#L2630) | subquery driving-table materialization |
| [TSqlExecutor.prg:2935](../_FiveSql2/src/TSqlExecutor.prg#L2935) | CTE materialization |
**A/B 벤치마크** ([_FiveSql2/test/bench_bulk.prg](../_FiveSql2/test/bench_bulk.prg), 10k 행 테이블, 20 iteration)
| 테스트 | PRG 루프 (before) | SqlBulkInsert (after) | 개선 |
|--------|------------------:|---------------------:|-----:|
| `BULK_CTE_10k` (5k 행 materialize) | 260 ms | **194 ms** | **1.34x** |
| `BULK_SUBQ_10k` (2k 행 materialize) | 121 ms | **107 ms** | **1.13x** |
*쿼리당 환산*: CTE 10k에서 `(260-194)/20 = 3.3ms`/쿼리 절감. 5000행 × 3컬럼 = 15000 boundary crossing → ≈ **220ns/crossing** 절감 (VM 프레임 setup 비용).
**기존 bench_sql(100행 규모) 효과 미미**: 40행 × 2컬럼 = 80 crossing × 220ns ≈ 18µs/쿼리 절감. 4.3ms 쿼리에서 <1% noise. 실제 이득은 **N이 커질수록 선형 증가**.
**Harbour 호환 보장**
- NIL 원소 스킵 동작 정확히 보존
- 행/필드 길이 불일치 처리 동일
- `Flush()` 호출로 `dbCommit()` 대체 — 동일한 디스크 반영 시점
**검증**
- `go test ./...` — ALL PASS
- FiveSql2 — 43/43
- Harbour compat — 56/56
## 진행 순서
1.#1 Hash 스토리지 — 완료
2.#2 SqlDistinct — 완료
3.#3 SqlRowCompare NULL·혼합타입 — 완료
4.#4 ASort 정확성/특화 — 완료
5.#5 AScan fast-path — 완료
6.#6 RAT 재검토 — 변경 없음
7.#7 SqlExprHasAgg Go walker — 완료
8.#8 SqlBulkInsert — 완료 (Tier 4)
9.#9 SqlBulkUpdate — 완료 (Tier 4)
10.#10 MEMRDD 자동 임포트 — 완료 (Tier 4 인프라)
11.#11 PcCompile 결과 캐시 — 완료 (Tier 4 회수 최적화)
12.#12 SQL 플랜 캐시 + HbDeepClone — 완료 (Tier 4 상위 계층)
13.#13 파라미터 바인딩 입증 — 완료 (기존 기능 + 플랜 캐시 결합 효과)
14.#14 CTE → MEMRDD — 완료 (디스크 임시파일 제거)
15.#15 SqlWindowPartitions Go RTL — 완료 (윈도우 파티션 빌드)
16.#16 SqlWindowSortPartition Go RTL — 완료 (윈도우 정렬)
17.#17 SqlGroupRows Go RTL — 완료 (GROUP BY 그룹 빌드)
18.#18 SqlComputeAggSimple Go RTL — 완료 (집계 함수 fast-path)
19.#19 SQL 스칼라 헬퍼 Go RTL — 완료 (IsTrue/CmpEq/CmpLt/Coerce×3)
20.#20 SQL 템플릿 자동 파라미터화 — 완료 (리터럴 → `?` + 플랜 캐시 공유)
21.#21 TSqlLexer Go 포팅 + 결합 — 완료 (#20 효과 증폭)
22.#22 SqlWindowAssignRank Go RTL — 완료 (ROW_NUMBER/RANK/DENSE_RANK)
23.#23 HbDeepClone 성능 개선 — 완료 (스칼라 재귀 스킵 + 해시 키 공유)
24.#24 WA 캐시 + 지연 commit — 완료 (B12 INSERT **48x**)
25.#25 Plan pcode 캐시 + Flush 지연 — 완료 (B13 UPDATE **48x**)
26.#26 SELECT plan pcode 캐시 — 완료 (SELECT fast-path 캐시 확장)
27.#27 SqlEvalHaving Go RTL — 완료 (효과 미미, 복잡 HAVING 워크로드용)
28.#28 VM Function() + Symbol 캐시 — 완료 (전역 3-15%, 호출 경로 핫패스)
**전체 계획 완료 (2026-04-17).** 각 단계 후 `go test ./...` + FiveSql2 43/43 + Harbour compat 필수 원칙 준수.
### #9 SqlBulkUpdate Go RTL — 2026-04-17 완료
**구현** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `SqlBulkUpdate(aFieldPositions, pcWhere, aValuePcodes) → nAffected` — WHERE + SET 값 모두 컴파일된 pcode를 받아 Go 내부에서 스캔·평가·PutValue. `FastFieldGetter` 설치로 pcode 내부 `FieldGet`도 인터페이스 디스패치 없이 `*DBFArea.GetValue` 직접 호출
- 공유 모드면 `LockRecord`/`UnlockRecord`로 레코드 락, 독점 모드면 생략
- 비-DBF 워크에어리어는 제네릭 `hbrdd.Area` 경로로 fallback
**PRG 연결** ([_FiveSql2/src/TSqlExecutor.prg RunUpdate](../_FiveSql2/src/TSqlExecutor.prg))
- `::oTxn:IsActive()` 이면 **반드시 PRG 루프** (txn 로그 보존) — 안전 게이트
- txn 없으면 WHERE + 각 SET 값을 `SqlExprToPrg``PcCompile`로 pcode 변환 시도
- 하나라도 실패(복잡 CASE·서브쿼리·UDF 등) 시 PRG 루프로 폴백
- 모두 성공 시 `SqlBulkUpdate(aFPos, pcWhere, aValuePc)` 한 번 호출
**A/B 벤치마크** ([_FiveSql2/test/bench_bulk_upd.prg](../_FiveSql2/test/bench_bulk_upd.prg), 10k 행 테이블)
| 테스트 | PRG 루프 | SqlBulkUpdate | 개선 |
|--------|---------:|--------------:|-----:|
| 2500행 매치 × 50회 (쓰기 지배적) | 2140 ms | 2153 ms | noise (쓰기 비용 동일) |
| 10k 전체 매치 × 10회 | 508 ms | **145 ms** | **3.5x** |
| 0행 매치 × 100회 (WHERE만) | 2288 ms | **214 ms** | **10.7x** |
**관찰**
- WHERE 평가가 지배적일수록 이득 큼 (pcode가 PRG EvalExpr보다 훨씬 빠름)
- 쓰기 지배 워크로드는 `PutValue` 디스크 I/O가 병목 — RTL 효과 제한적
- B13 소형 벤치(100행 × 1 매치)는 PcCompile 오버헤드 회수 전에 끝나 개선 미미. 규모 커질수록 선형 이득
**Harbour 호환 보장**
- Txn 활성 시 반드시 PRG 경로 → 롤백·savepoint 시맨틱 보존 (`test_sql1999.prg 4b SAVEPOINT + ROLLBACK TO` 통과)
- 복잡 표현식도 PRG 폴백 → SqlExprToPrg가 NIL 반환 시 기존 동작 그대로
- 공유/독점 모드에 맞춘 락 정책
**검증**
- `go test ./...` — ALL PASS
- FiveSql2 — 43/43
- Harbour compat — 56/56
### #10 MEMRDD 자동 임포트 — 2026-04-17 완료
Five 컴파일러가 생성하는 Go 코드에 `_ "five/hbrdd/mem"` 블랭크 임포트를 자동 추가. 기존에는 mem 패키지가 `init()`에서 드라이버 등록하지만 아무도 임포트하지 않아 MEMRDD 미등록 상태였음.
**변경** ([compiler/gengo/gengo.go:103-120](../compiler/gengo/gengo.go#L103-L120))
```go
if hasXBaseCommands(file) {
g.imports["five/hbrdd/mem"] = true
g.importAlias["five/hbrdd/mem"] = "_"
}
```
이제 PRG에서 `USE "mem:x" VIA "MEMRDD"` / `dbCreate("mem:x", aStruct, "MEMRDD")` 즉시 사용 가능. 임시테이블·CTE materialize의 in-memory 전환 기반.
### #11 PcCompile 결과 캐시 — 2026-04-17 완료
**동기**: [#9 SqlBulkUpdate](#9-sqlbulkupdate-go-rtl--2026-04-17-완료)가 쿼리마다 `SqlExprToPrg``PcCompile`을 호출. 파서+preprocess+genpc가 ~50200µs — B13(100행 × 1 매치) 같은 소형 쿼리에서는 RTL 절감분을 먹어치움.
**구현** ([hbrtl/pcexpr.go](../hbrtl/pcexpr.go))
```go
var pcCompileCache sync.Map // map[string]*hbrt.PcodeFunc
```
- 캐시 히트 시 파서/genpc 건너뛰고 즉시 pointer 반환
- `sync.Map` — read-mostly 패턴에 최적. `PcodeFunc`는 컴파일 후 불변이라 goroutine 간 공유 안전
- 무한 캐시 — 실제 워크로드의 distinct 표현식 수는 작음 (쿼리 템플릿 수준). LRU는 이후 필요시 추가
**효과 (bench_sql, 1000 iteration 반복 쿼리)**
| 쿼리 | 캐시 전 | 캐시 후 | 개선 |
|------|-------:|-------:|-----:|
| B13 UPDATE (1행 매치, SqlBulkUpdate 경로) | 4309 ms | **3536 ms** | **18%** |
| B12 INSERT | 3033 ms | 3001 ms | noise (파서가 별도 — 이 캐시는 PcCompile만 다룸) |
SqlBulkUpdate가 PcCompile을 호출하는 쿼리(B13, 대량 UPDATE)에서 직접 이득. 타 벤치는 PcCompile을 호출하지 않거나 이미 1회만 호출해서 효과 없음.
**Harbour 호환 보장**: `PcodeFunc`는 immutable, 소스 문자열이 키. 동일 소스 → 동일 결과 보장. 컴파일 실패 시 캐시에 저장 안 함.
**검증**
- `go test ./...` — ALL PASS
- FiveSql2 43/43 · Harbour compat 56/56
### #12 SQL 플랜 캐시 + HbDeepClone — 2026-04-17 완료
**동기**: `TFiveSQL:Execute`가 매 호출마다 lex + parse 실행. 반복 쿼리(B1~B11, B13~B15 등 벤치 대부분)는 동일 SQL 텍스트 → 파싱을 한 번만 수행하고 재사용하면 큰 이득.
**안전 이슈**: 기존 코드 주석(`Parse — no caching (plan trees are mutated during execution)`)이 경고했듯 `SqlFoldConst` 등이 AST 노드를 in-place 변경 ([_FiveSql2/src/TSqlExpr.prg:75-151](../_FiveSql2/src/TSqlExpr.prg#L75-L151)). 캐시에서 포인터를 그대로 반환하면 첫 실행이 캐시를 오염.
**구현**
- Go RTL `HbDeepClone(xVal) → xNewVal` ([hbrtl/array.go](../hbrtl/array.go)) — `deepCloneValue` 재귀로 Array/Hash를 element별 복제. 스칼라는 불변이라 그대로 반환. `HBDEEPCLONE` · `HB_DEEPCOPY` 두 이름으로 등록
- PRG 정적 캐시 `s_hPlanCache` ([_FiveSql2/src/TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg))
- 히트: `HbDeepClone(s_hPlanCache[cSQL])` 반환 → Run이 마음껏 변경해도 캐시 불변
- 미스: 파싱 후 `HbDeepClone(hQuery)`를 캐시에 저장, 원본은 Run에 넘김
```prg
STATIC s_hPlanCache := { => }
...
IF hb_HHasKey( s_hPlanCache, cSQL )
hQuery := HbDeepClone( s_hPlanCache[ cSQL ] )
ELSE
...parse...
s_hPlanCache[ cSQL ] := HbDeepClone( hQuery )
ENDIF
```
**효과 (bench_sql, 1000 iteration, µs/query)**
| # | 쿼리 | #11 이전 | #12 적용 후 | 개선 |
|---|------|-------:|----------:|-----:|
| B1 | `SELECT *` | 148 | **113** | 1.31x |
| B2 | `WHERE` | 166 | **85** | **1.95x** |
| B3 | `ORDER BY` | 178 | **96** | **1.85x** |
| B4 | `GROUP HAVING` | 877 | 731 | 1.20x |
| B5 | `DISTINCT` | 122 | **81** | 1.51x |
| B6 | `INNER JOIN` | 357 | **231** | 1.55x |
| B7 | `CTE simple` | 4415 | 4000 | 1.10x |
| B9 | `ROW_NUMBER` | 1134 | 1017 | 1.11x |
| B11 | `SUM OVER` | 621 | **493** | 1.26x |
| B13 | `UPDATE` | 3536 | **3301** | 1.07x |
| B15 | `CTE+WIN+JOIN` | 5751 | 5502 | 1.04x |
*B12 (INSERT)는 문자열 리터럴 i 값이 매 iteration마다 달라 캐시 미스 — 향후 파라미터 바인딩 도입 시 대상.*
**Harbour 호환 보장**
- 캐시 히트든 미스든 Run이 받는 hQuery는 항상 pristine — 첫 번째든 천 번째든 동일한 파싱 결과 트리
- 공유 상태 (static hash)는 동일 프로세스 내 호환 — 멀티스레드 시 PRG STATIC이 goroutine-local이라는 Five의 스레드 모델 준수
- SqlFoldConst를 포함한 모든 in-place 변경이 `HbDeepClone` 덕분에 격리
- **43/43 FiveSql2 · 56/56 Harbour compat · go test ALL PASS**
### #13 파라미터 바인딩 입증 — 2026-04-17
기능은 기존에 이미 있었음 (`five_SQL(cSQL, aParams)` + 파서 `?` 토큰 처리 + `ND_PAR` 노드). #12 플랜 캐시와 결합 시 동일 SQL 템플릿은 100% 캐시 히트. 사용자가 문자열 연결 대신 `?` 전환 시 자동으로 이득.
**A/B (1000 iteration)**
| 패턴 | 문자열 연결 | 프리페어 `?` | 개선 |
|------|----------:|-----------:|-----:|
| INSERT | 3214 ms | **2881 ms** | 1.12x (쓰기 I/O 지배) |
| SELECT | 254 ms | **161 ms** | **1.58x** (파서 지배) |
**응용 가이드**: 반복 DML/SELECT는 `?` + `aParams` 패턴 권장. 문자열 연결은 매번 파싱 비용 발생.
### #14 CTE → MEMRDD — 2026-04-17 완료
**동기**: CTE materialize가 매 쿼리마다 `dbCreate`/`USE`/`CLOSE`로 디스크 임시 .dbf 생성. 단일 프로세스에서 불필요한 디스크 오염 + syscall 비용.
**구현**
- `MaterializeCTE` ([TSqlExecutor.prg:2287-2315](../_FiveSql2/src/TSqlExecutor.prg#L2287-L2315)), `SqlMaterializeSubquery` ([:2672-2675](../_FiveSql2/src/TSqlExecutor.prg#L2672-L2675)), `MaterializeRecursiveCTE` ([:2969-2988](../_FiveSql2/src/TSqlExecutor.prg#L2969-L2988)) — 3곳 `dbCreate(cFile+".dbf")``dbCreate("mem:"+cTmpFile, aStruct, "MEMRDD")`, `USE``dbUseArea(.T., "MEMRDD", ...)`
- Sub-executor의 CTE 재오픈 경로 ([:1222-1245](../_FiveSql2/src/TSqlExecutor.prg#L1222-L1245)) — MEMRDD 우선 시도, 실패 시 legacy `.dbf` fallback (기존 디스크 임시파일 호환)
- **버그 수정 1**: `SqlBulkInsert` ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))가 `*dbf.DBFArea`에 하드 타입 어설션 → MEMRDD 경로에서 0 반환. 일반 `hbrdd.Area` 인터페이스 fallback 추가
- **버그 수정 2**: MEMRDD Create ([hbrdd/mem/memrdd.go](../hbrdd/mem/memrdd.go))가 호출자가 넘긴 DBF 스타일 `PadR(name, 10)`을 그대로 저장 → `FieldPos("ID")``"ID "`와 미스매치. Create에서 `TrimRight(name, " ")` 정규화
**효과**
- bench_sql B7/B8 (40행 CTE): 4000→4075 / 3947→4010 ms — noise (OS 파일 캐시로 소형 DBF도 이미 빠름)
- bench_bulk 5000행 CTE: 194→185 ms — 5% 개선
- **정확성**: 디스크에 `__cte_*.dbf` 임시파일 생성 제거 → 동시 실행 시 파일명 충돌 없음, 권한 이슈 없음
**Harbour 호환**
- aTables[i][1] 값(cTmpFile)은 여전히 "__cte_xxx" 형태 — 외부 로직 변경 없음
- sub-executor fallback 경로로 기존 `.dbf` 파일 운용 케이스도 호환
- `test_sql1999.prg 43/43` 전부 통과 (CTE/RECURSIVE CTE/CTE+Window/CTE+JOIN 포함)
### #15 SqlWindowPartitions Go RTL — 2026-04-17 완료
**구현** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `SqlWindowPartitions(aRows, aPartColIdx) → aPartitions` — PARTITION BY 컬럼 인덱스 배열을 받아 행-인덱스별 그룹 배열 반환. 첫 등장 순서 보존
- `appendValueHashKey` 공유로 키 구성이 `SqlValToStr`와 byte-for-byte 일치
-`aPartColIdx` → 전체 행을 단일 파티션으로 반환 (no-PARTITION-BY 시맨틱)
**PRG 호출부** ([ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg))
- PARTITION BY 컬럼을 한 번만 `SqlFindColIdx`로 해석해 `aPartColIdx`로 묶음
- `SqlWindowPartitions( aRows, aPartColIdx )` 1회 호출로 루프 전체 대체
- `FOR EACH aPartIdx IN hb_HValues(hPartitions)``FOR EACH aPartIdx IN aPartitions`
**bench_sql 효과** (직전 → 현재)
| # | 쿼리 | 직전 (µs) | 지금 (µs) | 개선 |
|---|------|---------:|---------:|-----:|
| B7 | CTE simple | 4075 | **127** | **32.1x** |
| B8 | RECURSIVE CTE | 4010 | **155** | **25.9x** |
| B9 | ROW_NUMBER | 1030 | 971 | 1.06x |
| B10 | RANK PARTITION | 1249 | 1145 | 1.09x |
| B11 | SUM OVER | 492 | 384 | 1.28x |
| B15 | CTE+WIN+JOIN | 5271 | **2547** | **2.07x** |
**대형 개선 원인 해설**: 이 변경 자체는 B9~B11의 PARTITION BY 루프 하나만 건드렸지만, B7/B8/B15 같은 CTE 쿼리에서도 큰 개선이 나타남. CTE materialize 후 재실행 경로에서 stale `__cte_*.dbf` 디스크 파일이 섞여 있던 이전 상태 → #14 MEMRDD 도입 + 깨끗한 상태에서 재측정된 효과로 판단. 반복 실행 확인 결과 수치는 안정적 (127ms ± 1%).
### #16 SqlWindowSortPartition Go RTL — 2026-04-17 완료
**구현** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `SqlWindowSortPartition(aRows, aPartIdx, aSortSpec) → aPartIdx` — 파티션 배열을 `sort.SliceStable`로 in-place 정렬. `aSortSpec`: 사전 해석된 `{nCol, lDesc}`
- NIL 시맨틱: PRG `SqlWinRowCmp` byte-for-byte 일치 (NIL = 가장 큼 → NULLS LAST in ASC, NULLS FIRST in DESC)
- 혼합 타입: PRG 동일하게 `ValType` 미일치 시 다음 정렬 키로 이동
- Stable sort로 `SqlWindowPartitions`의 first-seen 순서 보존
**PRG 호출부** ([ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg))
- ORDER BY 컬럼 인덱스를 윈도우 컬럼마다 한 번만 해석 → `aSortSpec`
- 파티션마다 `SqlWindowSortPartition(aRows, aPartIdx, aSortSpec)` 호출
- 기존 `ASort(aPartIdx,,, {|a,b| SqlWinRowCmp(...) < 0})` PRG 블록 경로 제거
**bench_sql 효과**
| # | 쿼리 | 직전 (µs) | 지금 (µs) | 개선 |
|---|------|---------:|---------:|-----:|
| B9 | ROW_NUMBER | 971 | **270** | **3.60x** |
| B10 | RANK PARTITION | 1145 | **462** | **2.48x** |
| B11 | SUM OVER (no ORDER BY) | 384 | 382 | noise (정렬 미사용) |
| B15 | CTE+WIN+JOIN | 2547 | **2158** | 1.18x |
**개선 원인**: ASort가 PRG 블록 콜백을 O(N log N)번 호출. 블록마다 `SqlWinRowCmp``SqlFindColIdx` 컬럼 재해석이 반복됨. Go 경로는 (i) 블록 경계 크로싱 제거, (ii) 컬럼 인덱스를 쿼리당 1회만 해석. 20행 파티션 × 5개 × 100 비교 ≈ 500 크로싱/쿼리 → 0.
**Harbour 호환**: 43/43 FiveSql2 · 56/56 compat · go test ALL PASS. NULL 순서 · mixed-type 처리 모두 PRG `SqlWinRowCmp`와 동일.
### #17 SqlGroupRows Go RTL — 2026-04-17 완료
**구현** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `SqlGroupRows(aRows, aGroupColIdx) → aGroupedRows` — 행 값(인덱스 아님) 기준으로 그룹 배열 반환. first-seen 순서 보존
- `appendValueHashKey` 공유로 `SqlValToStr` 시맨틱 byte-for-byte 일치
-`aGroupColIdx` → 전체 행이 단일 그룹 (no-GROUP-BY aggregate 시맨틱)
**PRG 호출부** ([TSqlAgg.prg GroupBy](../_FiveSql2/src/TSqlAgg.prg))
- PRG의 `cKey += SqlValToStr(...) + "|" → hb_HHasKey → AAdd` 루프를 `SqlGroupRows(aRows, aGroupIdx)` 1회 호출로 대체
- `FOR EACH aGroupRows IN hb_HValues(hGroups)``FOR EACH aGroupRows IN aGroupedRows`
- 집계·HAVING 평가는 PRG 유지 (복잡한 표현식 처리 — 서브쿼리, CASE, COUNT DISTINCT 등)
**bench_sql 효과**
| # | 쿼리 | 직전 (µs) | 지금 (µs) | 개선 |
|---|------|---------:|---------:|-----:|
| B4 | GROUP_HAVING | 738 | **659** | 1.12x |
| B10 | RANK PART (GROUP도 씀) | 462 | **397** | 1.16x |
| B15 | CTE+WIN+JOIN | 2158 | 2065 | 1.04x |
**한계**: 소규모 벤치(100행·5그룹)에선 집계 계산·HAVING 평가가 PRG에 남아 이득 제한적. 대량 행·다중 그룹 키 쿼리에선 선형 이득 증가.
**Harbour 호환 보장**
- 첫 등장 순서 유지 → 결과 행 순서 불변
- SqlValToStr 시맨틱 동일 → 그룹 키 동등성 불변
- ROLLUP/CUBE/GROUPING SETS 경로는 재귀 호출로 동일하게 이 함수를 이용
- 43/43 · 56/56 · go test ALL PASS
### #18 SqlComputeAggSimple Go RTL — 2026-04-17 완료
**구현** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `SqlComputeAggSimple(aGR, nCol, cFunc)` — 사전 해석된 컬럼 인덱스로 단일-pass 집계 루프. 타입 구분 비교 (`compareValuesNonNil`)로 PRG `SqlCmpLt`와 일치
- 지원: COUNT / SUM / AVG / MIN / MAX (컬럼 인자 한정)
- COUNT(*) / 전체 카운트는 nCol=0 케이스로 처리
- SUM/AVG는 모든 값 NIL이면 NIL 반환 (SQL 표준)
**PRG 호출부** ([TSqlAgg.prg ComputeAgg](../_FiveSql2/src/TSqlAgg.prg))
```harbour
IF nCol > 0 .AND. xArg[ 1 ] == ND_COL .AND. ;
( cFunc == "COUNT" .OR. cFunc == "SUM" .OR. cFunc == "AVG" .OR. ;
cFunc == "MIN" .OR. cFunc == "MAX" )
RETURN SqlComputeAggSimple( aGR, nCol, cFunc )
ENDIF
/* 복잡한 인자(CASE/BIN/UDF) + GROUP_CONCAT은 기존 PRG 경로 유지 */
```
**bench_sql 효과**
| # | 쿼리 | 직전 (µs) | 지금 (µs) | 개선 |
|---|------|---------:|---------:|-----:|
| B4 | GROUP_HAVING | 659 | **585** | 1.13x |
| B14 | COUNT | 374 | 364 | 1.03x |
| B15 | CTE+WIN+JOIN | 2065 | **1980** | 1.04x |
**Harbour 호환 보장**
- PRG SqlCmpLt 시맨틱 그대로 (타입 내 비교, NIL 제외)
- SQL 표준 NULL 처리 (SUM of all NULLs = NULL)
- 복잡 인자·GROUP_CONCAT은 자동으로 PRG fallback — 기능 회귀 없음
- 43/43 · 56/56 · go test ALL PASS
### #19 SQL 스칼라 헬퍼 Go RTL — 2026-04-17 완료
**구현** ([hbrtl/sqlhelpers.go](../hbrtl/sqlhelpers.go))
- `SqlIsTrue(x)` — SQL truthiness (NIL/빈문자/0 → false)
- `SqlCmpEq(a,b)` — 대소문자 무시 + trim + cross-type N↔C 강제변환 비교
- `SqlCmpLt(a,b)` — 대소문자 무시 + trim + cross-type 미만 비교
- `SqlCoerceForCmp(x)` — 비교용 정규화 (trim + upper for strings)
- `SqlCoerceNum(x)` / `SqlCoerceStr(x)` — 스칼라 변환
**버그 수정**: 초기 구현에서 `at == bt` 같은 타입-엄격 검사로 **NumInt vs Double 비교 실패**. PRG `ValType`은 둘 다 "N"으로 반환하지만 Go `Type()``tInt` vs `tDouble` 구분. `IsNumeric() && IsNumeric()`로 일원화해 수정. 테스트 6b (`SUM(amount) > 1000`) 회귀로 발견.
**PRG 정의 제거** ([TSqlFunc.prg](../_FiveSql2/src/TSqlFunc.prg)) — 심볼 충돌 방지. 기존 호출자는 자동으로 Go RTL 해결.
**효과**: 벤치 대부분이 이미 pcode 경로 사용 중이라 제한적 — B13 UPDATE 3451 → 3341 µs (~3%). 주 이득은 HAVING 평가 + 비-컴파일 가능 복잡 표현식 경로에서 누적됨. 대량 행·복잡한 WHERE의 장기 워크로드에서 누적 효과 예상.
**Harbour 호환 보장**
- 43/43 · 56/56 · go test ALL PASS
- PRG 원본과 byte-for-byte 동일 (NULL/cross-type/trim-upper 전부 유지)
### #20 SQL 템플릿 자동 파라미터화 — 2026-04-17 완료
**구현** ([hbrtl/sqlhelpers.go](../hbrtl/sqlhelpers.go))
- `SqlExtractTemplate(aTokens) → { cKey, aParams }` — 토큰 배열을 in-place 수정:
- `TK_TEXT`/`TK_NUM` 리터럴 → `TK_QMARK` 치환 + 값을 aParams에 순서대로 추출
- 비-리터럴 토큰은 타입+이름을 템플릿 키에 포함해 셰이프 구분
**PRG 연결** ([TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg))
- 사용자가 명시 `aParams`를 넘기지 않았으면 자동-파라미터화 경로:
1. 렉싱 1회
2. `SqlExtractTemplate`로 템플릿 키 + 추출된 aParams
3. 템플릿 키로 플랜 캐시 조회; 히트 시 `HbDeepClone`; 미스 시 파싱 후 저장
4. 추출된 aParams를 Executor에 전달 → `ND_PAR` 노드가 정상 해석
- 명시 `aParams`가 있으면 기존 cSQL-키 경로 유지 (prepared statement 그대로)
**효과**
| 쿼리 | 이전 µs | 현재 µs | 개선 |
|------|--------:|-------:|-----:|
| B12 INSERT (concat) | 3037 | 3086 | noise (lex 비용이 parse 절감 상쇄) |
| PREPARED_INSERT | 2881 | **2755** | 1.05x (plan cache 히트율 상승) |
| PREPARED_SELECT | 161 | 166 | noise |
**한계**
- 1000회 반복 벤치에서 lex 비용 (PRG SubStr 기반 렉서)이 parse 절감과 비슷한 수준 → 단독 효과 미미
- 진짜 이득은 다양한 쿼리 셰이프가 반복되는 실제 워크로드 (예: 보고서 쿼리) — 플랜 캐시 히트율 상승
- 향후 **렉서 Go 포팅** 또는 **SQL 텍스트 직접 정규화**(pre-lex normalization)로 lex 비용도 절감 가능
**Harbour 호환 보장**
- 기능적으로 동일 — PRG가 `?` + aParams 수동 사용했을 때와 완전 동등
- 사용자 명시 aParams와 충돌 방지 (별도 경로)
- 43/43 · 56/56 · go test ALL PASS
### #21 TSqlLexer Go 포팅 + lex-and-extract 결합 — 2026-04-17 완료
**구현** ([hbrtl/sqlhelpers.go](../hbrtl/sqlhelpers.go))
- `lexSQL(s string) []hbrt.Value` — Go byte-level FSM. TSqlLexer:Tokenize의 PRG SubStr 기반 버전 대체. 동일한 `{nType, cText}` 배열 반환
- 공백/라인주석/블록주석 스킵
- 문자열 리터럴 (`''` 이스케이프)
- 숫자 리터럴 (정수/소수)
- 식별자/키워드 (대문자 정규화)
- 브래킷 식별자 `[name]`
- 파라미터 `?`
- 단일/다중 문자 연산자 (`<=`, `<>`, `>=`, `!=`, `||`)
- `SqlLexerTokenize(cSQL) → aTokens` — 단순 lex RTL
- `SqlLexAndExtractTemplate(cSQL) → {aTokens, cKey, aParams}` — lex + 템플릿 정규화 1회 결합 (PRG→Go boundary 크로싱 감소)
**PRG 연결** ([TFiveSQL.prg](../_FiveSql2/src/TFiveSQL.prg))
- `TSqlLexer:New + Tokenize + GetTokens` 제거 (PRG 렉서 객체 미사용)
- 자동-파라미터화 경로: `SqlLexAndExtractTemplate` 1회 호출로 {tokens, cKey, aParams} 획득
- 명시 aParams 경로: `SqlLexerTokenize`로 단순 lex 후 파서에 전달
**bench_sql 효과**
| # | 쿼리 | 이전 (µs) | 지금 (µs) | 개선 |
|---|------|---------:|---------:|-----:|
| B8 | RECURSIVE CTE | 156 | 148 | 1.05x |
| B10 | RANK PART | 400 | 377 | 1.06x |
| **B11** | **SUM OVER** | 382 | **336** | **1.14x** |
| B12 | INSERT | 3086 | 2991 | 1.03x |
| B13 | UPDATE | 3480 | 3415 | 1.02x |
| B15 | CTE+WIN+JOIN | 1981 | 1922 | 1.03x |
**bench_prep_sql (1000 iter)**
| 패턴 | 이전 µs | 지금 µs | 개선 |
|------|-------:|-------:|-----:|
| CONCAT_INSERT | 3142 | **2996** | 1.05x |
| CONCAT_SELECT | 260 | 251 | 1.04x |
| PREPARED_INSERT | 2755 | 2734 | 1.01x |
| PREPARED_SELECT | 166 | 161 | 1.03x |
**CONCAT_INSERT(2996)가 이제 PREPARED_INSERT(2734)에 근접** — 자동 파라미터화 효과가 드러남. 남은 차이는 쓰기 I/O 비용(둘 다 동일).
**Harbour 호환 보장**
- 토큰 형식·타입 코드 완전 일치 (`FiveSqlDef.ch`의 TK_* 상수와 동일)
- 문자열 이스케이프·주석·연산자 파싱 byte-for-byte 매치
- 기존 PRG TSqlLexer는 유지 (아직 사용 안 하지만 외부 참조 가능)
- 43/43 · 56/56 · go test ALL PASS
### #22 SqlWindowAssignRank Go RTL — 2026-04-17 완료
**구현** ([hbrtl/sqlscan.go](../hbrtl/sqlscan.go))
- `SqlWindowAssignRank(aRows, aPartIdx, aSortSpec, nColIdx, cFunc)` — 정렬된 파티션 한 번 순회하며 랭크 값을 결과 컬럼에 기록
- `aSortSpec`#16 `SqlWindowSortPartition`에서 사전 해석된 `{nCol, lDesc}` 배열 그대로 재사용
- 3개 함수 통합 처리:
- `ROW_NUMBER`: 순서대로 1..N 배정
- `RANK`: 동일 값 → 같은 랭크, 다음은 k+1
- `DENSE_RANK`: 동일 값 → 같은 랭크, 다른 값 → rank+1
**PRG 호출부** ([ApplyWindowFunctions](../_FiveSql2/src/TSqlExecutor.prg))
- 3개 CASE를 통합된 Go 호출로:
```harbour
CASE cFunc == "ROW_NUMBER" .OR. cFunc == "RANK" .OR. cFunc == "DENSE_RANK"
SqlWindowAssignRank( aRows, aPartIdx, aSortSpec, nColIdx, cFunc )
```
- 기존 PRG 루프 + per-row `SqlWinRowsEqual` 호출 제거
**bench_sql 효과**
| # | 쿼리 | 이전 (µs) | 지금 (µs) | 개선 |
|---|------|---------:|---------:|-----:|
| B9 | ROW_NUMBER | 270 | 265 | 1.02x (이미 빠름 — 동순위 검사 불필요) |
| **B10** | **RANK PARTITION** | 377 | **309** | **1.22x** |
| B11 | SUM OVER | 336 | 334 | noise (RANK 미사용) |
**Harbour 호환**
- NIL 동등 검사 정확 재현 (NIL == NIL, NIL ≠ non-NIL)
- 타입 내 비교는 `compareValuesNonNil` 재사용 → 기존 `SqlCmpLt == 0` 시맨틱과 일치
- 43/43 · 56/56 · go test ALL PASS
### #23 HbDeepClone 성능 개선 — 2026-04-17 완료
**변경** ([hbrtl/array.go deepCloneValue](../hbrtl/array.go))
- 배열 원소가 Array/Hash일 때만 재귀 호출; 스칼라(문자열/숫자/논리/Date/NIL)는 슬롯 복사만
- 해시 키는 복사하지 않고 공유 (Five Hash는 문자열/숫자 키가 일반적 + 불변)
**배경**: 플랜 캐시 히트마다 전체 hQuery 트리를 deep clone. AST 노드는 `{nKind, xVal, xLeft, xRight, xExtra}` 5-element 배열이고 대부분 내부 요소가 스칼라. 기존 구현은 스칼라에도 함수 호출+switch 수행.
**bench_sql 효과** (측정 내 변동 ±1%, 누적 영향)
| 쿼리 | 이전 (µs) | 지금 (µs) | 개선 |
|------|---------:|---------:|-----:|
| B1 SELECT * | 117 | 106 | 1.10x |
| B8 RECURSIVE CTE | 150 | 149 | noise |
| B12 INSERT | 3082 | 3000 | 1.03x |
| B15 CTE+WIN+JOIN | 1930 | 1932 | noise |
작은 쿼리에선 노이즈 수준이지만, 대형 AST (복잡한 CTE, 깊은 서브쿼리)에선 선형적 이득.
**Harbour 호환**
- Hash 키 공유는 PRG Hash API가 키 변경 비공식(삽입 후 변경은 보통 `Delete`+`Insert`)이라 안전
- 43/43 · 56/56 · go test ALL PASS
### #24 WA 캐시 + 지연 commit — 2026-04-17 완료 (최대 이득)
**Go RTL 설계** ([hbrtl/sqlwacache.go](../hbrtl/sqlwacache.go))
- 프로세스-전역 `sync.Mutex` 보호 `map[alias→nWA]` + `enabled bool`
- 노출 심볼: `SqlWACacheEnable` / `Disable` / `IsEnabled` / `Get` / `Put` / `Invalidate` / `CloseAll`
- 기본 **disabled** — 회귀 테스트·일회성 스크립트는 기존 동작 보존
- 사용자가 opt-in 해야 활성화 (벤치·서버·긴-러닝 앱용)
**PRG 연결** ([TSqlExecutor.prg SqlExecOpenTable/CloseTable](../_FiveSql2/src/TSqlExecutor.prg))
- `SqlExecOpenTable(cTable, cAlias)`: 캐시 enabled + 적중 → `dbSelectArea` 재사용; 아니면 `dbUseArea` 후 `Put`
- `SqlExecCloseTable(cAlias, nWA)`: 캐시 켜지고 등록된 WA면 **스킵**, 아니면 기존처럼 close
- RunInsert / RunUpdate / RunDelete 3곳 교체
- **핵심 트릭**: 캐시 enabled 시 각 메서드 끝의 `dbCommit()`도 스킵 (`IF ! SqlWACacheIsEnabled()`) → 배치 commit은 `dbCloseAll()` 시점 또는 사용자 통제
**bench_sql (cache 활성화)**
| 쿼리 | 이전 (µs) | 지금 (µs) | 개선 |
|------|---------:|---------:|-----:|
| **B12 INSERT** | 3011 | **62** | **48.6x** |
| B13 UPDATE (1행 매치) | 3439 | 3275 | 1.05x (scan/eval가 지배, commit 비중 작음) |
| SELECT 계열 | 거의 동일 | 거의 동일 | - |
**누적 개선 (2026-04-08 원본 → 현재)**: B12 INSERT **4,319 → 62 µs = 69.7x**
**Harbour 호환 보장**
- **opt-in** 설계라 기본 동작 불변 (43/43 통과)
- 열린 WA lifecycle은 사용자 책임 (CREATE/DROP 시 `SqlWACacheInvalidate` 호출; 프로세스 종료 시 `dbCloseAll`)
- CREATE/DROP TABLE 자동 invalidate 통합은 향후 확장 — 현재는 명시적 API 제공
**사용 예** (bench_sql.prg)
```harbour
SqlWACacheEnable()
FOR i := 1 TO 1000000
five_SQL( "INSERT INTO log VALUES (?, ?, ?)", { ... } )
NEXT
SqlWACacheDisable()
dbCloseAll() // flush + close all
```
**검증**: go test ALL PASS · FiveSql2 43/43 (cache disabled 기본) · Harbour compat 56/56
### #28 VM Function() + Symbol hoist — 2026-04-17 완료 (전역 3-15%)
**동기**: 모든 PRG 함수 호출이 `Function()` + `FindSymbol`을 거침. 프로파일 결과 기존 코드는 (a) 매 호출 heap 할당 (`args := make([]Value, nArgs)`), (b) 인자 pop-push 왕복, (c) 매 호출 `strings.ToUpper` + `RWMutex` + `map` 조회. 개별 비용은 작지만 쿼리당 수십~수백 회 호출 → 누적이 큼.
**구현**
- [hbrt/call.go Function](../hbrt/call.go): pop-push dance를 single-slice-shift로 대체. N+2 pops + N pushes → 1 copy + sp 조정. Heap alloc 제거. Resolve 성공 시 `sym.Func = fn`으로 캐시
- [hbrt/vm.go GetSym](../hbrt/vm.go): `GetSym(cache **Symbol, name string)` — `*cache != nil`이면 즉시 반환, 아니면 `FindSymbol` 후 캐시 (nil은 init-order 재시도 허용해 캐시 안 함)
- [hbrt/thread.go pushPendingSym](../hbrt/thread.go): depth=1 호출(대부분)을 위한 scalar fast slot 추가
- [compiler/gengo/gengo.go emitPushSymbol](../compiler/gengo/gengo.go): `t.PushSymbol(t.VM().FindSymbol(%q))` → `t.PushSymbol(t.GetSym(&_sym_file_NAME, %q))`. 파일별 prefix로 다중-PRG 빌드 충돌 방지
**bench_sql 효과**
| 쿼리 | 이전 (µs) | 현재 (µs) | 개선 |
|------|---------:|---------:|-----:|
| B1 SELECT * | 114 | **97** | 15% |
| B4 GROUP_HAVING | 584 | **554** | 5% |
| B8 RECURSIVE CTE | 150 | **141** | 6% |
| B10 RANK PART | 310 | **296** | 5% |
| B11 SUM OVER | 335 | **320** | 4% |
| B14 COUNT | 295 | **281** | 5% |
전체 쿼리에서 3-15%. 평균 ~5% 단순 개선이지만 모든 쿼리가 혜택.
**버그 수정 (구현 중 발견)**
- `pendingSymFast = nil`이 "빈 슬롯"과 "nil 심볼 저장" 양쪽 의미라 ambiguous. nil 심볼은 슬라이스 경로로 fallback해 해결
- `GetSym`이 nil resolve 결과 캐시하면 init 순서 문제로 영구 미해결 가능. nil 캐시 생략해 재시도 허용
- gengo의 중복 호출 분기가 `t.PushSymbol(varName)`을 직접 emit해 lazy init 우회. 모든 호출을 `emitPushSymbol` 통일
**Harbour 호환 보장**: 43/43 · 56/56 · go test ALL PASS.
### #27 SqlEvalHaving Go RTL — 2026-04-17 완료 (효과 미미)
**구현** ([hbrtl/sqlscan.go SqlEvalHaving](../hbrtl/sqlscan.go))
- `SqlEvalHaving(xE, aNewRow, aCols, aGR, aFN, aParams) → {lOk, lPass}`
- Go AST walker: ND_LIT / ND_NIL / ND_COL / ND_FN(COUNT/SUM/AVG/MIN/MAX with ND_COL 인자) / ND_BIN (AND/OR/비교) / ND_UNI (NOT/-)
- 지원 외 노드 만나면 `lOk=.F.` 반환 → PRG fallback
**PRG 연결** ([TSqlAgg.prg EvalHaving](../_FiveSql2/src/TSqlAgg.prg))
- 먼저 Go RTL 호출, lOk=.T.이면 결과 사용, 아니면 기존 `EvalHavingExpr` PRG walker
**프로파일 결과** (별도 측정, 5 그룹 × 3 컬럼 GROUP BY)
| 패턴 | 이전 | 현재 | 차이 |
|------|----:|----:|----:|
| GROUP BY + HAVING | 589 µs | 579 µs | -10 µs (1.7%) |
| GROUP BY no HAVING | 568 | 565 | noise |
**솔직한 평가**: HAVING 자체가 B4 전체의 ~21 µs (3.6%) 차지. Go RTL 호출 오버헤드 (array allocation × 그룹 수 + PRG-Go 경계)가 절감을 상쇄. 단일 비교 HAVING에선 PRG 버전이 이미 충분히 빠름.
**의미 있는 케이스**: 복잡한 HAVING (다중 AND/OR, CASE) 또는 많은 그룹 (수백~수천)에서 이론적 이득 있음. 현재 벤치 규모에선 드러나지 않음.
**Harbour 호환**: 43/43 · 56/56 · go test ALL PASS. 복잡한 케이스 PRG fallback으로 안전.
### #26 SELECT 경로 plan pcode 캐시 — 2026-04-17 완료
**구현** ([TSqlExecutor.prg RunSelect fast path](../_FiveSql2/src/TSqlExecutor.prg))
- `aFP` (`TryBuildFieldPositions` 결과) + `pcW` (`TryCompileWhere` 결과)를 `s_hDmlPcodeCache[cCacheKey + "#sel"]`에 캐시
- 반복 SELECT (같은 SQL 템플릿)는 `SqlExprToPrg` AST walk 생략
**효과**: 벤치에선 이미 PcCompile source-string 캐시가 있어 소폭 변화. 복잡한 WHERE 표현식을 가진 대량 반복 SELECT 워크로드에서 추가 이득.
**Harbour 호환 보장**: 43/43 · 56/56 · go test ALL PASS
### #25 Plan pcode 캐시 + SqlBulkUpdate Flush 지연 — 2026-04-17 완료 (B13 48x)
**동기**: B13 UPDATE가 1행-매치임에도 3275µs. 프로파일 결과 **SqlBulkUpdate Go RTL 내부 `dbfArea.Flush()`가 1.6ms 차지** — macOS APFS fsync 비용이 매 UPDATE 누적.
**구현 2단계**
**(A) Plan-level pcode 캐시** — `TSqlExecutor.cCacheKey` + `s_hDmlPcodeCache`
- `TFiveSQL:Execute`가 plan-cache 키(`cKey` 또는 `cSQL`)를 `::oExec:cCacheKey`로 전달
- `RunUpdate`가 cache hit 시 `SqlExprToPrg` + `PcCompile` 왕복 완전 생략
- 처음 1회 컴파일 후 `{set_fpos, set_pc, where_pc}` stash
**(B) Go RTL Flush 지연** — 실제 B13 병목의 주요 원인
- `SqlBulkUpdate`가 `waCacheEnabledSafe()` 체크 후 `Flush()` 스킵
- 캐시 활성 시 PRG `dbCommit`과 Go `Flush` 모두 배치됨 → `dbCloseAll()`에서 일괄 fsync
**효과**
| 쿼리 | 이전 (µs) | 현재 (µs) | 개선 |
|------|---------:|---------:|-----:|
| **B13 UPDATE** | 3275 | **67** | **48.9x** |
| B12 INSERT | 62 | 62 | 유지 |
| 기타 | 동일 | 동일 | - |
**프로파일 (10k iter, 단일 행 UPDATE)**
- 이전: 1640 µs/call, SqlBulkUpdate Go 내부 1602µs
- 이후: 14.4 µs/call, SqlBulkUpdate 7.6 µs
**Harbour 호환 보장**
- WA 캐시 disabled 기본값에서 Flush 여전히 수행 (durability 유지)
- 43/43 · 56/56 · go test ALL PASS
- 사용자 `dbCommit` / `dbCloseAll` 명시 호출 시 배치된 변경 정상 flush
### #27 VM in-place stack ops + symbol hoist — 2026-04-18 완료 (글로벌 3-15%)
**동기**: RTL 대변동 이후 남은 VM 핫패스. 매 산술/비교 연산마다 `pop→pop→push` 왕복이 값 복사 3회 + 슬롯 클리어 3회. `PushSymbol(FindSymbol("QOUT"))` 패턴이 매 호출 심볼 테이블 조회.
**구현** (5 커밋: `1f63c7f` `15aa6dd` `54b35f9` `0fd3698` `523d3fc`)
- **Symbol hoist**: 함수 호출 심볼을 package-level `var _sym_<file>_<NAME> *hbrt.Symbol`로 캐시. 런타임 첫 호출 시 resolve → cache
- **`Function()` stack shift**: 인자를 아래로 shift해 PushNil 슬롯 덮어쓰기 (pop+push 왕복 제거)
- **In-place ops**: `Plus/Minus/Mult/Equal/NotEqual/Less/Greater/GreaterEqual/And/Or` 전부 `sp-=2; *dst = op(stack[sp], stack[sp+1])` 형태로. tInt+tInt fast path 포함
- **`ArrayGen/EvalBlock`**: 단일 copy + 스택 shift
**효과**
- 모든 벤치에서 글로벌 **3-15%** 감소
- 특히 tight loop (FOR/WHILE + 산술/비교)에서 최대 이득
**Harbour 호환 보장**: 43/43 · 56/56 · go test ALL PASS
### #28 gengo 컴파일타임 peephole — 2026-04-18 완료 (9 커밋)
**동기**: Harbour는 PRG 소스를 pcode로 컴파일 → VM이 해석하는 2단계 구조. Five는 PRG를 직접 Go 소스로 emit (`gengo`). 이 Go 출력 자체를 최적화하면 런타임 VM 비용이 사라진다.
**구현** (`a0acdf0` `111ab8a` `3f8ef7d` `1b6d913` `c3a9eb3` `67a9855` `7e4079f` `b829ed4` `6974ff9`)
| Peephole | 예시 | emit 변화 |
|---|---|---|
| 상수 폴딩 | `2 + 3 * 4` | `t.PushInt(14)` (1 op) |
| 단항 `-` 리터럴 | `-42` | `t.PushInt(-42)` |
| `x := x + y` | → `t.LocalAdd(idx)` | 3 ops → 1 op |
| 죽은 `IF .F.` / `ELSEIF .F.` / `.T.` | 본문만 emit | wrapper 제거 |
| `.AND. / .OR.` 리터럴 LHS | `.F. .AND. x` → `PushBool(false)` | short-circuit |
| `.NOT. <literal>` | `PushBool(true/false)` | Not() 호출 제거 |
| `DO WHILE .T.` / `.F.` | `for {}` / 본문 삭제 | PopLogical per-iter 제거 |
| 문자열 concat 좌향 재결합 | `"a" + x + "b" + "c"` → `"a" + x + "bc"` | 2 Plus 절약 |
| **상수 전파 LOCAL** | `LOCAL k := 100; FOR i TO k` → `FOR i TO 100` | 모든 fold를 깨움 |
| 죽은 store 제거 | 상수 전파된 LOCAL의 init 자체 skip | 프롤로그 6 ops 절약 |
**효과**: 개별 peephole은 1-3%. 누적 수익은 크지 않지만(FiveSql2 벤치 SELECT 1-7%) **컴파일타임 비용 0**이고 모든 Five 프로그램에 적용.
**Harbour 호환 보장**: 43/43 · 56/56 · go test ALL PASS. 상수 전파 walker는 알 수 없는 AST 노드에서 abort(보수적) — 이상한 함수는 단순히 최적화 대상에서 빠질 뿐 오류 없음.
### #29 SELECT 경로 WA 캐시 확장 — 2026-04-18 완료 (단일 테이블 SELECT 2x)
**동기**: WA 캐시(`SqlWACacheEnable`)는 DML 경로만 커버. SELECT은 매 쿼리 `TSqlExecutor:OpenTable → dbUseArea → ... → CloseOpened → dbCloseArea` 수행. 프로파일 결과 **`rtlDbCloseArea` + `munmap`이 SELECT CPU의 ~30%**.
**구현** (`f27c96c` TSqlExecutor.prg)
- `OpenTable`이 성공 시 WA 캐시 활성화 상태면 `SqlWACachePut(alias, nWA)`만 호출하고 `aOpened`에 추가 안 함
- `CloseOpened`는 `aOpened`만 순회하므로 캐시 보유 영역은 건드리지 않음 — 다음 쿼리 `Select(alias) > 0` → `OpenTable` 자체 스킵
- AcquireTemp-생성 alias(`FA_####`)는 캐싱 제외 (매 쿼리 alias 변경 → 캐시 히트 불가능 + 무한 누적)
**효과 (bench, 1000 iters, median of 3)**
| 벤치 | 이전 (µs) | 현재 (µs) | 개선 |
|---|---:|---:|---:|
| B1_SELECT_STAR | 82 | 41 | **2.0x** |
| B2_WHERE_FILTER | 78 | 35 | **2.2x** |
| B3_ORDER_BY | 90 | 48 | **1.88x** |
| B5_DISTINCT | 75 | 32 | **2.34x** |
| B7_CTE_SIMPLE | 120 | 77 | 1.56x |
| B9-B11 Window | — | — | 15-19% |
**Harbour 호환 보장**: 43/43 · 56/56. 캐시는 opt-in이라 기본 경로 semantics 불변.
### #30 JOIN temp-alias 안정화 — 2026-04-18 완료 (B6 1.67x)
**동기**: `TSqlAlias:AcquireTemp`가 매 호출 `FA_####`(순차 생성) 반환 → 같은 JOIN 쿼리를 1000번 반복해도 alias가 매번 바뀌어 WA 캐시 히트 불가능. B6 INNER_JOIN이 단일 SELECT 대비 5배 느린 원인.
**구현** (`6746ae4` TSqlAlias.prg)
- `AcquireTemp(cPurpose)`: cPurpose(대문자 테이블명)를 그대로 alias로 반환 — 단, 이번 쿼리의 `aSlots`에 이미 등록돼 있으면(`FROM emp e1, emp e2` 자기조인) `FA_####` fallback
- 첫 글자가 알파벳이 아니면 fallback (Harbour alias 규칙)
**효과**
- **B6_INNER_JOIN: 217 → 130 µs (1.67x)**
- B15_CTE_WIN_JOIN: 1678 → 1595 µs (-5%)
- B8 recursive CTE flat (서브-executor가 nDepth>1에서 여전히 fresh alias)
**Harbour 호환 보장**: 43/43 · 56/56.
### #31 Stat 루프 게이트 — 2026-04-18 완료 (CPU -17pp, wall-clock flat)
**동기**: 프로파일 재측정 결과 WA 캐시 후에도 `HbFileExists`가 28% → 20% CPU 유지. 두 루프가 범인:
1. `RunSelect` cleanup의 `__view_<table>.dbf` stat (VIEW 안 쓰는 쿼리에서도 실행)
2. CTE cleanup의 `__cte_<name>.dbf` stat (MEMRDD 사용하므로 파일이 없는데도 매번 stat)
**구현** (`9bb361b` `c4ae88e`)
- `TSqlIndex.lViewUsed` 플래그: `CheckView`가 `__view_*` temp를 materialize하면 set. cleanup은 플래그 set일 때만 실행
- `s_lCteDiskSeen` STATIC: CTE 레거시 disk fallback이 실제 발동했을 때만 set. cleanup은 플래그 set일 때만 실행
**효과 (pprof)**
- `rawsyscalln`: 48% → 32% → **8.5%** (두 게이트 누적 **40pp 감소**)
- `HbFileExists`: 28% → **dropped out of top**
- Wall-clock: flat (ENOENT stat은 Darwin 커널 캐시로 이미 저렴)
**의미**: Wall-clock 개선은 없지만 CPU 낭비 제거로 다른 워크로드와의 자원 경합 완화. 프로파일이 애플리케이션 코드를 넘어 Go runtime 프리미티브(`kevent`, `madvise`, `pthread_cond_*`) 중심으로 이동.
### #32 Go-native SqlIsAggName + FetchRow — 2026-04-18 완료 (집계/윈도우 1.3-1.7x)
**동기**: B4 GROUP+HAVING 프로파일링 (447µs — 단일 SELECT 대비 10x 느림)
- `SqlIsAggName` **8.7%**: `"," + c + "," ) $ ( "," + AGG_FUNCTIONS + "," )` — 매 함수 호출마다 2회 문자열 alloc + substring scan
- `FetchRow` **30%**: 캐시 binding이 있어도 PRG FOR 루프가 컬럼마다 `dbSelectArea` + `FieldGet` + `AllTrim` + `AAdd` 메서드 dispatch
**구현**
**(A) `SqlIsAggName` Go 네이티브** (`c84cde6`, hbrtl/sqlhelpers.go)
- 기존 `sqlexpr.go`의 `aggFuncSet` (`map[string]struct{}`) 재사용
- 입력이 이미 대문자면 할당 skip
**(B) `SqlFetchRowFast` Go 네이티브** (`935883b`, hbrtl/sqlscan.go)
- PRG `FetchRow`의 cache-hit 루프 전체를 단일 Go 호출로 대체
- Bound entry (`{nWA, nFPos}`): `wa.SelectByNum` + `area.GetValue` 직접
- Unbound entry (집계/표현식): `self:EvalExpr` via Send 콜백
- 문자열 값은 `strings.TrimSpace` 인라인 적용
- PRG `FetchRow`는 캐시 미스 fallback 경로만 유지
**효과 (median of 3)**
| 벤치 | 이전 (µs) | 현재 (µs) | 개선 |
|---|---:|---:|---:|
| B4_GROUP_HAVING | 418 | 327 | **1.28x** |
| B9_ROW_NUMBER | 191 | 120 | **1.59x** |
| B10_RANK_PART | 228 | 135 | **1.69x** |
| B11_SUM_OVER | 249 | 156 | **1.60x** |
| B14_COUNT | 235 | 219 | 1.07x |
| B15_CTE_WIN_JOIN | 1577 | 1452 | 1.09x |
**Harbour 호환 보장**: 43/43 · 56/56 · go test ALL PASS.
## 2026-04-18 세션 누적 효과 (baseline `3caadb2` 대비)
| 벤치 | Baseline (µs) | 최종 (µs) | 배수 |
|---|---:|---:|---:|
| B1_SELECT_STAR | 82 | 40 | **2.05x** |
| B2_WHERE_FILTER | 78 | 34 | **2.29x** |
| B3_ORDER_BY | 90 | 47 | **1.91x** |
| B4_GROUP_HAVING | 518 | 327 | **1.58x** |
| B5_DISTINCT | 74 | 31 | **2.39x** |
| B6_INNER_JOIN | 217 | 128 | **1.70x** |
| B7_CTE_SIMPLE | 120 | 75 | 1.60x |
| B8_RECURSIVE_CTE | 137 | 137 | flat |
| B9_ROW_NUMBER | 237 | 120 | **1.98x** |
| B10_RANK_PART | 276 | 135 | **2.04x** |
| B11_SUM_OVER | 295 | 156 | **1.89x** |
| B12_INSERT | 61 | 60 | flat |
| B13_UPDATE | 66 | 65 | flat |
| B14_COUNT | 270 | 219 | 1.23x |
| B15_CTE_WIN_JOIN | 1704 | 1452 | 1.17x |
12/15 벤치 의미있는 개선. 10개가 1.5x+, 6개가 2x 근처. B8/B12/B13만 flat (각각 재귀 sub-executor, DML — 이전 세션에서 이미 커버).
## 아직 남은 병목 (차기 검토 후보)
### 언어/VM 커버리지 (Phase A + B, 2026-04-18 완료)
- ✅ `::super:Method()` 디스패치 (`3a56bd3`)
- ✅ `METHOD ... INLINE expr` + `MESSAGE ... INLINE` (`34485cd`)
- ✅ `OPERATOR "<op>" ARG x INLINE` + VM 이항 연산 디스패치 (`66f045b`)
- ✅ `&var` / `&(expr)` 런타임 매크로 컴파일 (`e089c81`)
- ✅ `DATA x, y, z` 다중-이름 파싱 (`327f75b`)
- ✅ `DO(xTarget, args...)` 동적 디스패치 (`2a66252`)
- ⏸ `ACCEPT` — TUI 인터랙티브 입력. 사용자 판단으로 보류
- ✗ `HBARRAY` 등 11개 HB*TYPE — 조사 결과 Harbour 내부 클래스 팩토리로,
Five의 `SendBuiltin`이 이미 동등 기능 제공. 1.0 blocker 아님
### Phase C — Contrib 라이브러리 (별도 작업 예정)
[Five-1.0-Phase-C-TODO.md](Five-1.0-Phase-C-TODO.md) 참조. hbct (Clipper
Tools 40개) + hbnf (재무/통계 FT_*) + hbtip (네트워크 I/O) — 약 6-10일.
### 성능 영역 (Phase A/B와 무관, 계속 유효)
- **TSqlParser2 Go 포팅**: 가장 무거운 단계. PRG Pratt 파서 → Go 재구현
- **EvalExpr Go 네이티브화**: 현재 B4/B14/B15에서 17-20% CPU 차지. ~300-500줄 Go 포팅 + `Resolve` 등 executor 메서드 콜백 필요. 예상 10-15% 추가 수익, 리스크 높음
- **B8 recursive CTE**: sub-executor at `nDepth>1`이 여전히 temp alias 순환 — stable alias 확장으로 커버 가능한지 미검증
- **WA 캐시 auto-invalidate**: CREATE/DROP TABLE DDL에서 자동 invalidate
- **B15 복합 쿼리**: 각 단계 Go화 되었으나 JOINRECURSE/HASHJOIN이 여전히 PRG 메서드 dispatch. FetchRow 스타일 포팅 확장 가능
- **Go runtime 자체**: `kevent` 16.7%, `madvise` 9.6%, `pthread_cond_*` 12% — 애플리케이션 레벨에서 못 줄임. GC 튜닝(`GOGC`, `GOMEMLIMIT`) 정도만 가능