Files
five/docs/FiveSql2-Hybrid-Plan.md
CharlesKWON 8aaed994f4 perf(FiveSql2): hybrid fast path — 11x speedup on string WHERE scans
Implements hybrid execution model: keep AST tree-walk for SQL:2013+
features (Window, Recursive CTE, JOIN, aggregates) while compiling
simple SELECT hot paths to Go + pcode. See docs/FiveSql2-Hybrid-Plan.md
for the full architecture rationale (why not SQLite-style VDBE).

Hot path (single table, no joins/groups/aggregates):
  - TryBuildFieldPositions: resolves SELECT column list to FieldPos
    array once per query (bails to PRG loop on any complex expr).
  - TryCompileWhere + SqlExprToPrg: walks WHERE AST, emits equivalent
    PRG source, runs it through PcCompile to get a PcodeFunc.
  - SqlScan RTL: Go-native scan loop — GoTop/EOF/Skip/GetValue
    direct, ExecPcode per row for WHERE, result array pre-alloc.

WHERE compiler scope:
  - ND_LIT numeric/logical/string (string literals AllTrim'd to match
    SqlCmpEq CHAR-padding semantics; rejects embedded quotes/newlines)
  - ND_COL: CHAR fields auto-wrapped with AllTrim(FieldGet(n)) based
    on dbStruct() lookup cached once per query in aCompileStruct
  - ND_BIN: = <> != < <= > >= AND OR + - * /
  - ND_UNI: NOT -
  - Anything else (ND_FN, ND_CASE, ND_SUB, ND_PAR, LIKE, IN, IS NULL,
    BETWEEN, dates) returns NIL → falls back to PRG tree-walk.

Bench (50k rows, ~/tmp ext4):
                        Before      After     Speedup
  Numeric WHERE         ~150ms     11.7ms     ~13x
  String WHERE          119.3ms    10.5ms     11.4x
  No WHERE               -         14.6ms      -
  Raw RDD baseline        6.8ms     6.8ms      1.0x

Remaining gap to raw RDD (~1.5x) is structural: Value boxing, result
array construction, per-row ExecPcode frame overhead. Would need a
Value-pool or SoA refactor to close further.

Side fixes bundled:
  - TSqlIndex:FindExclusive short-circuited. Originally called
    dbInfo(DBI_FULLPATH)/DBI_SHARED which are unresolved symbols in
    Five (dbInfo is a stub, DBI_* never defined). Panic'd with
    "local variable index out of range: 0" whenever a standalone PRG
    had a workarea Used before calling five_SQL. 43-test masked the
    bug because it only reached FindExclusive with no open workareas.
    Restore the scan once dbInfo lands in hbrtl.
  - cmd/five/main.go: FIVE_KEEP_BUILD=1 env var keeps the temp Go
    project around for debugging gengo output.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:15:08 +09:00

176 lines
6.9 KiB
Markdown
Raw 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.
# FiveSql2 하이브리드 실행 모델 — 구현 계획
**Date:** 2026-04-14
**Decision Owner:** Charles KWON
**Status:** 확정 (user: "좋아요 그게 답입니다")
---
## 1. 아키텍처 결정
### 1.1 설계 원칙
FiveSql2는 **AST tree-walk 평가기**와 **pcode/Go 핫패스**를 공존시킨다.
SQLite 방식(AST 폐기 + 전량 VDBE 컴파일)은 **채택하지 않는다**.
### 1.2 왜 하이브리드인가
| 관점 | AST 유지 (FiveSql2) | VDBE 전량 (SQLite) |
|------|---------------------|-------------------|
| 신규 SQL 표준 추가 | evaluator에 CASE 추가 | 코드젠 + opcode 신설 |
| Window/RECURSIVE CTE | tree-walk로 자연 표현 | 서브프로그램·임시 커서 필요 |
| 단순 SELECT 성능 | 느림 (트리 워크) | 빠름 (op dispatch) |
| 구현 복잡도 | 낮음 | 매우 높음 |
FiveSql2는 SQL:1999 → SQL:2013 → 향후 표준 확장이 최우선이므로
**AST 기반 확장성**을 희생하지 않는다. 성능은 **핫패스 선별 하강**으로 해결.
### 1.3 역할 분담
```
┌─────────────────────────────────────────────────────┐
│ Parser (PRG) → AST (영속) │
│ │ │
│ ▼ │
│ TSqlExecutor.RunSelect (PRG) │
│ ├─ Window / CTE / Recursive → tree-walk evaluator │
│ ├─ GROUP BY / Aggregate → tree-walk evaluator │
│ ├─ JOIN → tree-walk evaluator │
│ │ │
│ └─ ★ Hot path (simple scan) │
│ ├─ TryBuildFieldPositions(aExprs) │
│ ├─ TryCompileWhere(xWhere) → PcCompile │
│ └─ SqlScan(fields, pcWhere) ── Go RTL ──┐ │
└─────────────────────────────────────────────────┼────┘
┌────────────────────────┐
│ hbrtl/sqlscan.go │
│ area.GoTop/EOF/Skip │
│ ExecPcode per row │
│ GetValue(idx) direct │
└────────────────────────┘
```
---
## 2. 현재 상태 (2026-04-14)
### 완료
- [x] `compiler/genpc/genpc.go``CompileExpr` 공개 API
- [x] `hbrtl/pcexpr.go``PcCompile` / `PcEval` RTL
- [x] `hbrtl/sqlscan.go` — Go 네이티브 스캔 루프
- [x] `hbrdd/dbf/dbf.go``FieldPosCache` O(1)
- [x] `_FiveSql2/src/TSqlExecutor.prg``TryBuildFieldPositions` / `TryCompileWhere` 메서드
- [x] Fast path 통합 (WHERE 없음 + 단순 projection 한정)
- [x] 회귀: 43/43 · 51/51 · Go tests ALL PASS
### 미완성
- [ ] `TryCompileWhere`가 항상 NIL 반환 — WHERE 있는 쿼리는 느린 경로
- [ ] `BindColumns/ResolveCache` 4-test 회귀 미해결
- [ ] 소형 PRG `TSqlIndex:FindExclusive` 패닉 (격리 벤치 차단)
---
## 3. 단계별 작업 계획
### Phase 1 — WHERE 컴파일러 (우선순위 최상)
**목표:** `TryCompileWhere`가 단순 WHERE를 pcode로 변환해 `SqlScan`에 넘김.
**범위:**
- ND_COL → `FieldGet(n)` (CHAR 비교 시 `AllTrim()` 자동 래핑)
- ND_LIT → 숫자/문자열 literal
- ND_BIN — `=`, `<>`, `!=`, `<`, `<=`, `>`, `>=`, `AND`, `OR`
- 그 외 (ND_FN, ND_CASE, ND_SUB, LIKE, IN, IS NULL, BETWEEN, ND_PAR) → NIL 반환 (fallback)
**의미 보존 원칙:**
- `SqlCmpEq`의 CHAR trim 규칙을 PRG 변환 시점에 반영 (비교 양변이 ND_COL이고 CHAR면 `AllTrim()` 래핑)
- NULL 비교는 첫 버전에서 미지원 — ND_NIL 포함 시 NIL 반환
- 타입 강제는 Five의 기본 연산자 오버로딩에 위임
**파일:**
- `_FiveSql2/src/TSqlExecutor.prg``METHOD SqlExprToPrg(xNode)` 신설
- `TryCompileWhere`에서 호출
**검증:** 43/43 유지 + simple WHERE 벤치 (50k rows, `salary > 50000`) pcode 경로 확인.
---
### Phase 2 — Projection 확장
**목표:** `TryBuildFieldPositions`를 넘어 단순 식 projection 지원.
**범위:**
- `SELECT a + b, c * 2 FROM t` — ND_BIN 산술식도 pcode로
- 설계 변경: `SqlScan`이 필드 인덱스 배열 대신 **pcode 배열**(projection 표현식) 수신
- `aSelectExprs []*PcodeFunc` 형태로 RTL 확장
**파일:**
- `hbrtl/sqlscan.go` — 시그니처 변경: `SqlScan(aProjs, pcWhere)`
- `_FiveSql2/src/TSqlExecutor.prg` — projection 빌더
**Risk:** SQL 함수(UPPER/ALLTRIM/SUBSTR)는 PRG 런타임에 존재. pcode ExecPcode가 이들 함수를 호출 가능한지 확인 필요.
---
### Phase 3 — BindColumns 회귀 해결
**목표:** 이전 세션의 `ResolveCache` PushLocal(0) 버그 근본 원인 파악.
**증상:** 4 tests panic at `class.go:278` Send. "Unresolved variable → PushLocal(0)".
**조사 항목:**
- CLASS 내부에서 Resolve를 캐시할 때 `self` 참조가 깨지는 경우
- `pendingParams` 순서와의 상호작용
- gengo가 캐시 변수를 local index 0으로 emit하는지
**파일:** `hbrt/class.go`, `compiler/gengo/gen_class.go` (조사 우선, 수정은 증거 기반)
---
### Phase 4 — 소형 PRG FindExclusive 패닉
**목표:** 격리 벤치가 가능하도록 class-system edge case 수정.
**증상:** 소형 PRG에서 `TSqlIndex:FindExclusive` "local variable index out of range: 0"
**영향:** 벤치 격리 차단. 43-test 통합 실행에서는 발생 안 함 → **Phase 1 종료 후** 진행.
---
### Phase 5 — 벤치 + 커밋
**목표:** 실측 데이터로 고속화 배수 확정 후 커밋.
**벤치 케이스:**
- 50k × 3 컬럼 DBF
- SELECT * (fast path)
- SELECT a, b WHERE ... (fast path after Phase 1)
- JOIN / GROUP BY / Window (fallback, 변화 없음 확인)
**커밋 규칙:** Phase 별 개별 커밋. CLAUDE.md의 3-테스트 게이트 매번 통과.
---
## 4. 비목표 (Non-goals)
- SQLite식 전량 VDBE 컴파일 — 거부
- AST 구조 변경 — SQL:2013 이상 확장 경로 보존
- 옵티마이저 전면 재작성 — 기존 `oIndex:TryIndexScan`, plan cache는 유지
- 동시성/트랜잭션 모델 변경 — 이번 범위 밖
---
## 5. 검증 게이트 (불변)
모든 Phase 종료 시 CLAUDE.md 규칙대로 3개 통과:
```bash
go test ./... # Go 유닛
./five build _FiveSql2/test/test_sql1999.prg _FiveSql2/src/*.prg -o /tmp/test_sql && \
cd ~/tmp && rm -f *.dbf __cte_*.dbf 2>/dev/null; /tmp/test_sql # 43/43
./five build tests/compat_harbour.prg -o /tmp/test_compat && /tmp/test_compat # 51/51
```
하나라도 실패 → 해당 Phase 롤백.