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

6.9 KiB
Raw Permalink Blame History

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)

완료

  • compiler/genpc/genpc.goCompileExpr 공개 API
  • hbrtl/pcexpr.goPcCompile / PcEval RTL
  • hbrtl/sqlscan.go — Go 네이티브 스캔 루프
  • hbrdd/dbf/dbf.goFieldPosCache O(1)
  • _FiveSql2/src/TSqlExecutor.prgTryBuildFieldPositions / TryCompileWhere 메서드
  • Fast path 통합 (WHERE 없음 + 단순 projection 한정)
  • 회귀: 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.prgMETHOD 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개 통과:

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 롤백.