# 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` 인덱스 추가, 삽입 순서 슬라이스 유지 | 50–100x | **완료 (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` | 100–300x | **완료 (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, 효과 미미)** | ### ❌ 제외 (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 비트셋 사전계산 | 5–15%지만 `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 워크로드용) **전체 계획 완료 (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가 ~50–200µ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 ### #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 ## 아직 남은 병목 (차기 검토 후보) - **TSqlParser2 Go 포팅**: 가장 무거운 단계. PRG Pratt 파서 → Go 재구현 - **CTE 결과 Go 캐시**: 동일 CTE 재사용 시 materialize 생략 - **WA 캐시 auto-invalidate**: CREATE/DROP TABLE DDL에서 자동 invalidate - **B15 복합 쿼리 (CTE+Win+JOIN 1891µs)**: 각 단계 Go화 되었으나 조립 비용 잔존