Files
five/OPTIMIZATION_TODO.md
CharlesKWON e79ced2e0c docs: log PP/std.ch round + LABEL/REPORT deferred
Record the 9-commit Phase B run that landed Harbour-style #command
rewrites for ERASE/RENAME/CLOSE/COMMIT/UNLOCK/LOCATE/CONTINUE/
REINDEX/PACK/ZAP/KEYBOARD/RUN plus COUNT/SUM/AVERAGE/COPY/SORT/
LIST/DISPLAY/TOTAL/JOIN/UPDATE — 13 commands that were silent
no-ops in the parser before this round.

Also catalog the 14 PP completeness fixes the rules surfaced
(partial-pattern false-match, blockify substitution, list-aware
smart-stringify and blockify, MarkerList/MarkerWordList in optional
clauses, multi-delimiter capture, line-continuation in directives,
no-progress iteration leak, unreferenced logify/blockify cleanup,
nested `[...]`).

LABEL / REPORT explicitly deferred — niche xBase output-formatting
engines whose `.lbl` / `.frm` binary readers and pagination/group
machinery would be ~800–1500 LOC for near-zero modern users. Parser
keeps the silent no-op behavior for both keywords; entry points
documented in OPTIMIZATION_TODO.md if a real demand ever appears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:52:30 +09:00

635 lines
56 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.
# Five — Optimization TODO (continue in next session)
Open this file in a future Claude session to resume the audit-driven
optimization work. The project-wide 절대 규칙 still applies: every
change must pass three gates before being accepted.
```bash
cd /Users/charleskwon/Projects/fivedev/five
go test ./...
./five build _FiveSql2/test/test_sql1999.prg _FiveSql2/src/*.prg -o /tmp/test_sql \
&& (cd ~/tmp && setopt NULL_GLOB; rm -f *.dbf *.ntx *.cdx; /tmp/test_sql)
./five build tests/compat_harbour.prg -o /tmp/test_compat && /tmp/test_compat
```
Targets: Go ALL PASS · FiveSql2 43/43 · Harbour compat 56/56.
---
## 완료 (2026-04-24/25: 잠재 버그 사냥 라운드 — Tier 1+2+3, 14/14)
14개 잠재 영역 전체 처리. 실버그 ~17건 추가 fix (CTE+DML, AGG-in-expr,
Recursive CTE+JOIN, FK ON UPDATE, NULL FK, Date arithmetic, Numeric
literal, Numeric overflow, Plan cache cap, RunUpdate LOCAL aliasing 등).
누적 이번 시즌 ~37건. 3 게이트 매번 그린 유지: Go test ALL PASS,
FiveSql2 43/43, Compat 56/56.
| 영역 | 버그 | Fix |
|------|------|-----|
| MERGE | `WHEN MATCHED AND <cond>` / `WHEN NOT MATCHED AND <cond>` 무시; `WHEN MATCHED THEN DELETE` 미구현; INSERT branch UNIQUE 검증 없음 | `TSqlExecutor.prg:RunMerge``match_condition`/`not_match_condition`/`matched_delete` 를 읽고 평가; INSERT branch 에 `SqlValidateUnique` 추가 |
| UPDATE PK | PK 컬럼이 `.fsc` UNIQUE 리스트에 없어서 PK 중복 UPDATE 가 silent 통과 | `TSqlDDL.prg:CreateTable` 에서 aPKCols → aUniqCols 자동 합침 |
| Recursive CTE | iter cap 50 → legit `seq 1..N` (N>50) silent truncation | `TSqlExecutor.prg:MaterializeRecursiveCTE` 50→10000 |
| Error quality | SELECT/UPDATE/DELETE/INSERT against missing table → panic 또는 silent garbage; INSERT 추가 값/없는 컬럼 → silent drop | `TSqlExecutor.prg` 에 pre-flight `File()` 체크 + INSERT 컬럼/값 길이 검증 |
| GROUP BY 표현식 | `GROUP BY UPPER(col)` / `GROUP BY col/N` / `GROUP BY <ordinal>` 모두 1 그룹으로 collapse | `TSqlAgg.prg:FindGroupIdx` 가 ND_LIT(numeric) ordinal + `SqlExprName`-based expression matching 지원 |
| LIMIT 0 | `LIMIT 0` 가 전체 반환 (parser default 0 == "no LIMIT" 와 충돌) | parser default `nLimit:=NIL`; executor `nLimit==N`이면 0/음수 → empty |
| DDL 충돌 | ALTER ADD 중복 컬럼 silent ok; ALTER DROP 없는 컬럼 → crash; CREATE TABLE 기존 파일 silent overwrite (데이터 손실); DROP TABLE 없는 파일 silent ok | 각각 pre-flight 체크 + `IF EXISTS` 지원 (DROP) |
| RETURN-from-SEQUENCE | `RETURN``BEGIN SEQUENCE` 안에서 unwind 안 되는 Five 동작 | SqlAlterAddColumn / SqlAlterDropColumn 에서 검증을 SEQUENCE 밖으로 이동 |
| **EXISTS regression** | 위 LIMIT 0 fix 의 부작용으로 `ExistsViaSemiJoin``hLifted["limit"] := 0` 이 "empty result" 로 해석 → 모든 lifted EXISTS false. 43-suite 가 못 잡아 한 라운드 잠복 | `hLifted["limit"] := NIL` |
| **DELETE+same-table subq** | EXCLUSIVE open 충돌로 subq 가 `__error__` envelope 반환; ND_SUB 가 그것의 [2][1][1] (= numeric error code 1005) 을 scalar 로 surface → WHERE 비교 모두 false. silent 데이터 보존 | `SqlExecOpenTable` SHARED 로 변경; ND_SUB 에 `__error__` 봉투 가드 추가 |
| **VIEW** | (a) CreateView 가 token-join 으로 SQL 저장 — string literal quote 손실 (`'eng'``eng`) → re-parse 시 syntax error. (b) SELECT FROM view 미구현 → `__error__` | (a) TK_TEXT 토큰 single-quote 로 다시 감싸고 `'` doubling. (b) `SqlMaterializeView` 추가 — `.fsv` 읽고 nested TFiveSQL 실행 후 MEMRDD temp 로 적재 |
| RTL/IO | `FRead(@buf, n)` byref 가 buffer 비워 둠 (관찰됨). View 본문 로딩에서 발견 | `MemoRead` 로 우회 |
| **CTE+DML** | `WITH cte AS (...) UPDATE/DELETE/INSERT ...` 가 status "TG" / "DELETE" 같은 garbage envelope 반환. (a) 파서가 WITH 뒤 SELECT 만 인식. (b) executor 의 RunInsert/RunUpdate/RunDelete 가 CTE materialize 안 함 → SET / WHERE 안의 subquery 가 CTE alias resolve 실패 | (a) `TSqlParser2.prg:ParseSelect` 의 WITH 분기가 trailing 키워드 보고 `ParseInsert/Update/Delete` 로 분기 + `cte`/`cte_recursive` 키 stash. (b) RunInsert/RunUpdate/RunDelete 진입부에 `MaterializeCTE` / `MaterializeRecursiveCTE` 호출 추가 |
| **AGG-in-expression** | `SELECT MAX(id)+1 / COUNT(*)+10 / SUM(v)-30 / MAX(id)*2` 모두 0 반환. (a) `ComputeAgg` 가 top-level 이 ND_FN 가 아니면 즉시 0 (ND_BIN/ND_UNI wrapper 무시). (b) hidden-columns 로직이 wrapped 케이스에서 source column 추가 안 함 → 추가했어도 row 에 없음 | (a) `TSqlAgg.prg:ComputeAgg` 진입부에 ND_BIN/ND_UNI/ND_FN(non-agg)/ND_LIT/ND_NIL recursive 분기 추가 — sub-aggregate 계산 후 SqlEvalRowExpr 로 wrapper 적용. (b) `TSqlExecutor.prg` hidden-cols 루프에 wrapped 케이스 분기 추가 — 전체 식에서 `SqlCollectColExprs` 로 ND_COL leaf 모두 hidden 으로 등록 후 LOOP. |
| **Recursive CTE + JOIN (aliased)** | `WITH RECURSIVE sub AS (...) SELECT s.id FROM sub s JOIN dep d ON ...` → "Table '__cte_sub' does not exist". `MaterializeRecursiveCTE``aTables[j][1]``__cte_<name>` 으로 rewrite — 사용자가 alias (`sub s`) 준 경우 `Select(user_alias)` 가 0 반환 → OpenTable 이 `__cte_<name>` DBF 찾으나 MEMRDD 전용이라 실패. 비-recursive CTE 는 동일 패턴이지만 rewrite 안 함. | `MaterializeRecursiveCTE` 의 aTables rewrite 블록 제거 — 원래 CTE 이름 유지하면 RunSelect 의 open 루프가 (a) `Select(cName)` 으로 이미 열린 area 찾거나 (b) MEMRDD fallback 으로 user alias 에 second workarea attach |
| **FK ON UPDATE** | DDL 파서가 `ON UPDATE` 절 미지원 (ON DELETE 만), executor 가 parent column UPDATE 시 children 에 RESTRICT/CASCADE/SETNULL 적용 안 함, FK validate 가 NULL 값 reject (SQL 표준 위반) | (a) `TSqlDDL.prg` parser 에 `DO WHILE ::IsKW("ON")` 루프 추가 — DELETE/UPDATE 양쪽 분기. (b) `.fsc` 형식: `FK:c:p_t:p_c[:on_del[:on_upd]]`. (c) `SqlFindReferencingFKs` 가 5번째 필드 (on_update) 반환. (d) 신규 `SqlEnforceUpdateRefs(parent, refs, hChanged)` 함수 — CASCADE 는 children UPDATE, SETNULL 은 children NULL set, RESTRICT 는 child row 있으면 차단. (e) `RunUpdate` PRG 루프에 변경 hash 빌드 + 호출. (f) `SqlValidateFKRecord` NIL 단락 — NULL FK 는 항상 만족 (표준). |
| **RunUpdate LOCAL slot aliasing** | RunUpdate 함수 top 에 새 LOCAL 추가하니 mid-function `LOCAL hUpdConstraints` 가 set 한 .T. 가 read 시점에 non-logical 로 surface → fast-path gate 에서 silent panic | mid-function LOCAL 모두 함수 top 으로 hoist. **Five 컴파일러의 known limitation: 함수 중간에 LOCAL 선언하지 말 것** — 이미 CLAUDE.md 의 인라인 IF 금지 규칙과 같은 카테고리. |
| **Date arithmetic** | `SELECT d + 7`, `d - 30`, `d - d`, `INSERT VALUES (DATE '...' + N)` 모두 wrong — 결과는 raw N (Date 가 SqlCoerceNum 으로 0 collapse). UPDATE 경로는 SqlCoerceToCol 이 D 컬럼에 N→D 변환 해서 우연히 동작. | `EvalExpr``SqlEvalRowExpr` 둘 다 `+`/`-` 분기에 `(D,N) → D + N`, `(N,D) → D + N`, `(D,N) → D - N`, `(D,D) → N (days)` 케이스 추가. SQL 표준 + Harbour 둘 다 같은 의미. |
| **Numeric literal in WHERE → 0** | `WHERE v = 0.1` (그리고 모든 fractional literal) silent 0 rows 반환. `SqlExprToPrg` 가 ND_LIT(N) 을 `AllTrim(Str(0.1))` 로 emit — Str(0.1) default 는 " 0" → AllTrim → "0" → pcode 가 `WHERE v = 0` 실행. 기존 43-suite 는 정수 비교만 사용해서 잠복. | ND_LIT(N) emit 을 `hb_NToS(xNode[2])` 로 변경 — decimal preserve. |
| **Numeric overflow silent** | `INSERT INTO n(N(4,0)) VALUES (99999999)` silent 0/garbage 저장. DBF N codec truncate. | RunInsert PRG 양 분기에 `Str(xVal, FieldLen, FieldDec)` 가 '*' 포함 시 `MakeError(SQL_ERR_GRAMMAR, "Numeric overflow: ...")` + dbDelete + close. |
| **NULL FK reject** | ON UPDATE SET NULL 으로 child.fk_col → NULL 쓰면 inner UPDATE 의 `SqlValidateFKRecord` 가 NULL 을 parent 에서 못 찾아 reject — child rollback. SQL 표준: NULL FK 는 항상 만족. | `SqlValidateFKRecord` 진입부에 `IF xValue == NIL RETURN .T.` 단락. |
| **Plan cache 무한 성장** | `s_hPlanCache` / `s_hDmlPcodeCache` 무제한 — 동적 SQL 많은 long-running 서버에서 메모리 leak. | `SQL_PLAN_CACHE_MAX = 1000` cap. 초과 시 전체 wipe (LRU 대신 — Five hash 가 insertion-order 보장 안 함). DML pcode 캐시 동일하게 cap + `SqlDmlPcodeCacheReset()` 헬퍼로 cross-module reset. |
### 추가 라운드 (2026-04-26: 잠재 영역 8개 stress test)
| 영역 | 버그 | Fix |
|------|------|-----|
| **HAVING wrapped agg** | `HAVING SUM(amt) + 1 > 200` (그리고 SUM 자체 단독) silent 0 rows. (a) `EvalHavingExpr` 가 산술/||(`+`,`-`,`*`,`/`,`||`) 분기 없음 → NIL 반환. (b) HAVING 내 aggregate 의 source column 이 hidden col 에 추가 안 됨 → ComputeAgg nCol=0. SELECT 에 같은 agg 있으면 hidden 으로 추가되어 우연히 동작. | (a) `TSqlAgg.prg:EvalHavingExpr` 에 산술/concat 분기 추가 + NULL 전파. (b) `RunSelect` 의 hidden col 루프 직후에 xHaving 도 `SqlCollectColExprs` 로 walk → ND_COL leaf 모두 hidden 등록. |
| **CASE wrapping aggregate** | `CASE WHEN COUNT(*) > 2 THEN 'many' ELSE 'few' END` → label = 0 (string literal 0 으로 collapse). ComputeAgg 가 ND_CASE 미지원 → fall through to default 0. | `ComputeAgg` 에 ND_CASE 분기 추가 — branches 각 cond/then 과 else 모두 recursive ComputeAgg. |
| **Multi-row scalar subquery silent** | `INSERT VALUES ((SELECT id FROM t), 100)` 가 t 의 첫 row 를 silent 사용 (t 가 multi-row). SQL 표준 위반. | ND_SUB scalar 분기에서 `Len(aSubResult[2]) > 1``OutErr()` warning + NULL 반환. silent 첫행→noisy NULL 으로. |
| **Self-FK INSERT silent pass** | `CREATE TABLE n (id, parent_id, FK(parent_id) REFERENCES n(id))``INSERT INTO n VALUES (5, 999)` (parent 없음) silent 통과. RunInsert 의 FK 검증이 `Len(aFields) > 0` 일 때만 동작 → positional INSERT 에서 skip. | RunInsert 의 FK loop 를 `FOR i := 1 TO FCount()` + `FieldName(i)` 로 변경 — 모든 컬럼 검증. SqlValidateFKRecord 가 비-FK 컬럼 cheap pass. |
| **Date + string ISO 비교** | `WHERE d = '2026-04-25'` → 0 rows (ISO 형식 매치 안 됨), `WHERE d > '2026-06-01'` → 모든 row (string compare 로 fall-through, '20260425' > '2026-06-01' 의 lexicographic 비교가 거꾸로). DToS 형식 (20260425) 만 동작. | `sqlhelpers.go:sqlCmpDateStr` + `sqlCmpLt` 의 D/C cross 분기에 `normalizeDateStr` 적용 — `-`, `/`, `.` 제거 후 비교. |
### 학습 포인트 (2026-04-26)
- **AGG 처리 fix 가 multiple sites 필요.** ComputeAgg + EvalExpr + EvalHavingExpr + hidden col 로직 + window agg + ORDER BY 등. 한 군데 fix 하면 다른 path 가 silent 0. 한 라운드에서 모두 cover 못 하면 다음 라운드에 또 발견.
- **Self-FK + multi-row INSERT 는 알려진 한계.** SqlValidateFKRecord 가 same area 의 dbGoTop/scan 으로 buffer state 잃음. dbCommit + RECNO save/restore 추가했어도 multi-row 는 여전히 fail. Single-row 와 cross-table 은 동작. Workaround: multi-row 대신 sequential single-row INSERT.
- **String compare 로 fall-through 가 silent corruption 의 가장 흔한 source.** Date/Number 대 String 비교가 잘 정의 안 되면 SqlCmpEq 가 false 반환하지만 SqlCmpLt 는 lexicographic 으로 동작 → WHERE 가 직관과 거꾸로 결과 반환. 항상 cross-type coercion path 명시.
### 추가 fix (보류 처리)
| 영역 | 버그 | Fix |
|------|------|-----|
| **ORDER BY wrapped agg** | `ORDER BY MAX(amt)+1 DESC` 정렬 안 됨 (insertion order). TryBuildSortSpec/TSqlSort:OrderBy 가 ND_COL 만 처리. | RunSelect hidden col 단계에서 ORDER BY 의 wrapped expression 을 `__ord_<i>__` alias 로 hidden col 추가 + `aResultExprs` & `aCols` 둘 다 mirror (GroupBy/aFieldNames 가 aCols 사용). aOrderBy 의 expression 을 ND_COL("__ord_<i>__") 로 in-place rewrite. RunSelect 끝에 `nUserCols` 로 hidden col trim. |
| **Window function arithmetic** | `SUM(amt) OVER () + 100` 가 100 반환. ApplyWindowFunctions 가 top-level ND_WINDOW 만 인식 → wrapped 시 placeholder 0 통과. | 신규 `SqlExtractWindow(xE, aWindows, cPrefix)` walker — 식 안의 ND_WINDOW 를 hidden alias `__win_<i>_<j>__` 로 substitute + extract. RunSelect 가 wrapped col index 를 `::aWrappedWindowCols` 에 등록. ApplyWindowFunctions 후 wrapped col 마다 `SqlEvalRowExpr` 로 row 다시 evaluate (post-window re-eval). |
| **Self-FK INSERT silent pass** | `INSERT INTO n VALUES (5, 999)` parent 없이 silent 통과. RunInsert 의 FK 검증이 named columns (`aFields`) 일 때만 동작. | RunInsert FK loop 를 `FOR i := 1 TO FCount()` + `FieldName(i)` 로 변경 — positional INSERT 도 검증. |
| **Self-FK multi-row INSERT** | `INSERT INTO n VALUES (1,NULL),(2,1)` 가 (2,1) FK 검증에서 fail (1006). SqlValidateFKRecord 의 dbGoTop 이 INSERT 중인 area 의 buffer state 깨뜨림 + 첫 row 가 buffer-only 상태라 못 봄. | self-FK 분기에서 (a) `dbCommit` 으로 buffer flush, (b) `__FK_<table>` 별도 area 강제 open (SHARED) — INSERT area 와 분리, (c) 검증 후 area close + 원래 area 복원. RECNO save/restore 는 fallback 경로용. |
| **Date+string ISO 비교** | `WHERE d = '2026-04-25'` (ISO) → 0 rows, `WHERE d > '2026-06-01'` → 모든 row. DToS 형식 (20260425) 만 동작. | `sqlhelpers.go:sqlCmpDateStr` + `sqlCmpLt` 의 D/C cross 분기에 `normalizeDateStr` 적용 — `-`, `/`, `.` 제거 후 비교. |
### 학습 포인트 (2026-04-26 추가)
- **Hidden col rewrite 시 aCols + aResultExprs 둘 다 update.** ORDER BY/HAVING/Window wrapper 처리 시 hidden col 를 추가하는 자리가 두 군데. aResultExprs 만 update 하면 GroupBy/aFieldNames rebuild 가 못 봄. 둘 다 mirror + nUserCols 로 trim.
- **Window arithmetic 은 fetch 와 ApplyWindowFunctions 의 평가 순서 문제.** Fetch 가 placeholder 0 으로 outer 식 평가 → 잘못된 결과 row 에 저장. ApplyWindowFunctions 는 ND_WINDOW 자리만 채움. 해결: post-window re-eval 단계 추가 — wrapped col 마다 row 재평가.
- **Self-FK validation 은 in-flight area 분리 필수.** SqlValidateFKRecord 가 같은 area 의 dbGoTop 사용하면 multi-row INSERT 의 dirty buffer 가 안 보이고 RECNO 도 깨짐. `__FK_<table>` 강제 open + dbCommit 으로 disk-only 검증이 정답. 비-self-FK 는 기존 path (already-open area 재사용).
### 추가 라운드 (2026-04-27: 8개 영역 stress test — 2 silent 버그 발견)
| 영역 | 버그 | Fix |
|------|------|-----|
| **CASE simple form** | `CASE v WHEN 0 THEN 'zero' WHEN 10 THEN 'ten' ELSE 'other' END` 가 1 row of NIL 반환 — 파서가 simple form (`CASE expr WHEN val ...`) 미지원. searched form (`CASE WHEN cond ...`) 만 처리. SQL 표준은 둘 다. | `TSqlParser2.prg:ParsePrimary` 의 CASE 분기에 simple form 감지 추가. CASE 다음에 WHEN 이 안 나오면 test expression 파싱, 각 WHEN val 을 ND_BIN(=, test_expr_clone, val) 로 desugar to searched form. |
| **Multi-column UNIQUE** | `CREATE TABLE u (a, b, UNIQUE(a, b))``INSERT (1,1)`, `INSERT (1,2)` 둘 다 fail — DDL 파서가 (a, b) 를 두 개의 single-col UNIQUE entry 로 저장 → SqlValidateUnique 가 a 와 b 각각 single 하게 검증. SQL 표준: UNIQUE TUPLE. | (a) DDL 파서가 multi-col UNIQUE 를 한 entry 로 comma-joined 저장 (`UNIQUE:a,b`). (b) `SqlValidateUnique` 가 entry 를 `hb_ATokens(',')` 로 split, 첫 col 이 cCol 일 때만 trigger (중복 검증 회피), 나머지 col 값을 caller area 에서 read, scan 시 모든 col 동시 매치. Single-col 은 `xValue` 직접 사용 (외부 caller 가 positioned record 없이 호출하는 경우 backward compat). |
### 검증 — 정상 동작 (6/6 + 부가)
| 영역 | 결과 |
|------|------|
| JOIN ON NULL (INNER, LEFT, anti-join via IS NULL) | 3/3 정확 |
| COALESCE / NULLIF / CASE searched form | 5/5 정확 (simple form fix 후) |
| Subquery in UPDATE SET (uncorrelated, correlated, arithmetic) | 3/3 정확 |
| Correlated UPDATE / DELETE WHERE EXISTS | 2/2 정확 |
| LIMIT/OFFSET edges (0, past-end, large, mid-range) | 6/6 정확 |
| TRUNCATE / DELETE FROM (no WHERE) | 정확 |
### 추가 라운드 (2026-04-27/28: Tier A NULL semantics 후속 + Tier B JOIN 확장)
| 영역 | 버그 | Fix |
|------|------|-----|
| **NOT BETWEEN with NULL** | `WHERE v NOT BETWEEN 10 AND 30` 가 v=NULL row 도 포함 (NOT(.F.) = .T.). SQL 표준: NULL → UNKNOWN → drop. ND_RANGE 평가가 NULL operand 전파 안 함. | EvalExpr 의 ND_RANGE 분기에 `IF xL/xR/xHi == NIL RETURN NIL` 가드 추가. NOT 은 이미 NIL→NIL 처리. |
| **IS DISTINCT FROM 미구현** | `WHERE v IS DISTINCT FROM NULL` 가 항상 0 rows. 파서는 ND_BIN("IS DISTINCT FROM") 노드 생성하지만 EvalExpr 에 분기 없음 → fall through to RETURN NIL → WHERE 항상 false. | EvalExpr 에 `IS DISTINCT FROM` / `IS NOT DISTINCT FROM` 분기 추가 — NULL-safe 비교 (둘 다 NULL → not distinct, 한쪽 NULL → distinct, 일반 값은 SqlCmpEq 역). |
| **CASE simple form 미지원** | `CASE v WHEN 0 THEN 'zero' WHEN 10 THEN 'ten' END` 가 1 row of NIL. 파서가 simple form 인식 안 함, searched form 만 처리. | `TSqlParser2.prg:ParsePrimary` 의 CASE 분기에서 CASE 다음에 WHEN 이 안 나오면 test expression 파싱, 각 `WHEN val``ND_BIN(=, test_clone, val)` 로 desugar to searched form. |
| **Multi-column UNIQUE** | `UNIQUE(a, b)` tuple 이 아니라 a 와 b 각자 single-col UNIQUE 로 저장 → `(1,1),(1,2)` 둘 다 fail. | (a) DDL 파서가 multi-col UNIQUE 를 한 entry comma-joined (`UNIQUE:a,b`) 저장. (b) `SqlValidateUnique` 가 entry 를 split, 첫 col == cCol 일 때만 trigger (중복 검증 회피), 나머지 col 값을 caller area 에서 read, scan 시 모든 col 동시 매치. Single-col 은 xValue 직접 사용 (외부 caller backward compat). |
### 검증 — 정상 동작
| 영역 | 결과 |
|------|------|
| LIKE ESCAPE / literal `%`, `_` 매칭 | 3/3 |
| IN list with NULL (3-value logic) | 2/2 |
| INNER / LEFT / RIGHT / FULL OUTER / CROSS JOIN | 5/5 |
| Window functions (ROW_NUMBER, RANK with ties, LAG, LEAD, ROWS BETWEEN sliding sum) | 동작 (LAG/LEAD `default` arg 는 NIL 으로 fall back — minor) |
| 4-table JOIN chain + star query (LEFT JOIN + GROUP BY) | 동작 |
### 시니어 audit 라운드 (2026-04-30: SQL 표준 conformance)
| 영역 | 버그 | Fix |
|------|------|-----|
| **COUNT(DISTINCT col)** | parser 가 DISTINCT keyword 무시 → aggregate 결과 0/NIL. SUM/AVG/MIN/MAX(DISTINCT) 도 동일. 매우 흔한 SQL 인데 silent wrong count. | (a) `TSqlParser2:ParsePrimary` function-call 분기에서 LPAR 직후 `DISTINCT`/`ALL` modifier 인식. ND_FN 의 5번째 slot 에 `lDistinct` flag 저장. (b) `TSqlAgg:ComputeAgg` 가 flag 보고 fast-path skip + per-value `hSeen` hash 로 dedup. |
| **UNION column count mismatch** | `SELECT a UNION SELECT a, b` 가 silent merge — 첫 SELECT 의 columns 만 keep, 둘째의 추가 column drop. SQL 표준은 error. | RunSelect 의 UNION 분기에서 `Len(aU[1]) != Len(aFieldNames)``MakeError(SQL_ERR_GRAMMAR, ...)`. |
| **DISTINCT + ORDER BY non-list col** | `SELECT DISTINCT grp ORDER BY id` 가 모든 row 반환 (DISTINCT 무력). ORDER BY hidden col `__ord_<i>__` 가 row 에 포함되어 DISTINCT 의 dedup hash 가 row 별 unique 인식. | DISTINCT 직전에 `nUserCols` trim 적용. ORDER BY 는 이미 적용 끝 — hidden col 더 이상 필요 없음. 마지막 trim block 은 redundant 로 남음 (no-op). |
### 검증 — 정상 (시니어 audit)
| 영역 | 결과 |
|------|------|
| HAVING without GROUP BY (implicit single group) | 4/4 |
| Self-JOIN (`emp e JOIN emp m`, 3-level chain, LEFT JOIN, WHERE on alias) | 4/4 |
| Implicit type coercion (N↔C, BETWEEN, decimal literal) | 6/6 |
| Recursive CTE cycle protection (iter cap 10000) | bounded |
| Large IN list (200+ items, NOT IN, empty IN) | 정상 |
| Boundary numeric (15-digit float64 limit), date (leap year, year boundary, +N arithmetic) | 정상 (Feb 29 non-leap → Mar 1 silent rollover 는 xBase 관습 — minor) |
### 보류 항목 종료 라운드 (2026-04-30)
이전 라운드의 모든 보류 항목 (4건) 완료. 시즌 마무리.
| # | 영역 | Fix |
|---|------|-----|
| 1 | **Derived table** (`FROM (SELECT...)`) | 파서는 이미 `__SUBQUERY__` 표시했고 `SqlMaterializeSubquery` 도 있음 — 단 (a) 1글자 alias 가 `Len(cAlias) <= 1` 분기에서 임시 alias 로 rename 되어 `Select(cAlias)` 가 못 찾음 → derived 의 cTable prefix `__drv_` 면 rename skip. (b) 임시 alias 로 open 후 close + reopen under user alias. (c) `::aOpened` 에 add 해서 next query 가 alias 충돌 안 일으킴. (d) JOIN right side 도 LPAR-SELECT 인식하도록 ParseFrom 의 JOIN 분기 확장. 3/3 derived 테스트 통과. |
| 2 | **Self-FK CASCADE depth 2+** | nested DELETE 가 same area record-pointer race. CASCADE 분기가 child PK pre-collect — `SqlGetSingleColPK(child)` 로 .fsc 의 single-col UNIQUE 추출, `SELECT pk WHERE fk=val` 로 list 모음, 각 PK 로 single-row DELETE 호출. PK 없으면 기존 multi-row nested DELETE fall back. 5-row org chain DELETE root → 0 remaining. |
| 3 | **LAG/LEAD default arg** | `LAG(v, 1, -1)` 의 -1 이 ND_UNI(-, ND_LIT(1)) 로 파싱돼 `aFuncArgs[3][1] == ND_LIT` check 실패 → xDefault=NIL. SqlEvalRowExpr 로 const expr 평가. negative literal / CAST / 기타 const 모두 동작. |
| 4 | **Feb 29 non-leap silent rollover** | `DATE '2025-02-29'` 가 CToD 의 xBase 관습 silent rollover (Mar 1) → 표준 위반. DATE 리터럴 파서가 round-trip 검증 (`DToS(d) == strip-separators(input)`), 불일치 시 NIL emit. |
### 시즌 종료
| 메트릭 | 값 |
|--------|-----|
| 누적 silent 버그 fix | ~62건 |
| 3 게이트 | Go test ALL PASS, FiveSql2 43/43, Compat 56/56 |
| 보류 영역 | 0 (모두 종료) |
### 추가 라운드 (2026-04-29~30: Tier A/B/C audit)
| 영역 | 버그 | Fix |
|------|------|-----|
| **DROP TABLE 메타 누락** | `.dbf`, `.fsc`, `_pk/_uq.ntx`, memo (`.dbt/.fpt`) 만 cleanup, **`.cdx`** + `.fsv` 누락 → 다음 CREATE 가 stale `.cdx` 자동 attach (multi-row INSERT silent drop 과 같은 메커니즘). | DropTable 의 FErase 체인에 `.cdx`, `.fsv` 추가. |
| **CREATE TABLE fsc/cdx/memo 누락** | 첫 cleanup 라운드에서 `_pk.ntx`/`_uq.ntx` 만 sweep — `.cdx`, `.fsc`, `.dbt`, `.fpt` 잔존 → constraint 없는 CREATE 후 prior `.fsc` 의 stale UNIQUE/CHECK 가 여전히 enforce 되어 silent dup-reject. | CreateTable pre-flight cleanup 에 `.cdx`, `.fsc`, `.dbt`, `.fpt` 추가. |
| **TSqlIndex.FindExclusive prefix-substring** | `cTableLow $ cDbfName` (basename 비교 아닌 substring) → "c" 가 ".../cus.dbf" 안에서 .T. → 다른 테이블의 EXCLUSIVE lock 으로 잘못 인식 → 새 OPEN 이 -1 (locked) 반환. | basename strip ("/" 와 "\\") 후 정확한 `<table>.dbf` / `<table>` 비교. |
| **AlterTable type dispatch substring** | `cType $ "CHAR,CHARACTER,VARCHAR"` 가 cType="A" 같은 1-char value 도 매치. CreateTable 은 이미 fix (`","+cType+","` wrap), AlterTable 은 누락. | AlterTable type dispatcher 에도 comma-wrap 적용. DOUBLE/FLOAT/REAL 도 추가. |
| **CREATE VIEW silent overwrite** | `CREATE VIEW v` 가 already exists 시 silent FCreate (overwrite). SQL 표준 위반. | already-exists 시 error 반환. `CREATE OR REPLACE VIEW v` 문법 추가 (executor dispatch + parser). |
| **MatchOrderByTag NIL panic** | `lTagDesc := dbOrderInfo(DBOI_ISDESC)` 가 NIL 반환 시 `! lTagDesc` panic (`argument error op: .NOT.`). 새로 build 된 인덱스에서 발생. | NIL → .F. 으로 default. |
### 검증 — 정상 동작
| 영역 | 결과 |
|------|------|
| ALTER TABLE + plan cache invalidation | 7/7 정확 (SqlBumpSchemaVer 동작) |
| ALTER ADD/DROP COLUMN 후 PK/UNIQUE 인덱스 정합성 | 6/6 |
| TSqlExecutor instance reuse | 안전 (매 query 새 New, Init 가 모든 instance var reset) |
| hQuery deep clone | 안전 (`HbDeepClone` per Execute) |
| Correlated subquery (scalar, EXISTS, NOT EXISTS, multiple per row) | 5/5 |
### 추가 fix — multi-row INSERT silent row drop (2026-04-28)
| 영역 | 버그 | Fix |
|------|------|-----|
| **prefix-glob index attach** | `TSqlIndex:AttachNTX` / `AttachCDX` / `OpenTable` 셋 다 `Directory(cTableLow + "*.ntx")` / `Directory(cTableLow + "*.cdx")` 로 매치 — `c*.ntx``c_uq.ntx` 외에 `cus_uq.ntx` 도 hit. sibling 테이블의 stale index 가 새 area 에 attach → SkipIndexed 가 stale key tree 따라 walk → record N 의 key 가 그 tree 에 없으면 silent skip. 현장 증상: `INSERT (r1) → INSERT (r2) → INSERT (r3) → SELECT` 가 2 rows 만 반환. INSERT 는 `affected_rows=1` 보고하지만 다음 SELECT 의 SqlScan 이 record 3 를 못 봄. 43-test 는 prior cleanup 으로 stale `_uq.ntx` 가 없어서 통과. CREATE TABLE 의 stale `_pk.ntx`/`_uq.ntx` cleanup 도 추가 (사용 환경 안전망). | `Directory` glob 제거 — convention 명 (`<table>_pk.ntx`, `<table>_uq.ntx` / `<table>.cdx`) 만 명시적으로 attach. RDD 결정 (`OpenTable`) 도 `File(cFileLow + ".cdx")` 로. ad-hoc index 는 explicit `SET INDEX TO` 로 사용. |
| **Generated function prefix** | `compiler/gengo` 가 PRG → Go 컴파일 시 함수 이름을 `HB_<NAME>` 으로 prefix — Harbour 의 `HB_FUNC` macro convention 답습. Five 의 정체성과 맞지 않음. | `gengo.go` + `gen_class.go` 의 prefix `HB_``FV_`. Generated symbol/function: `FV_MAIN`, `FV_TSQLEXECUTOR_RUNSELECT`, `FV_<class>_CTOR`, `FV_<class>_<method>`. Harbour 호환 RTL 함수 (hb_NToS 등) + `HB_FUNC` Go API + `#pragma BEGINDUMP` macro 는 그대로 유지 (외부 호환성). Stack trace 가 이제 `main.FV_MAIN` 등으로 표시. |
### 남은 보류 (다음 라운드)
| 영역 | 이유 |
|------|------|
| Self-FK CASCADE depth 2+ | Cascade 가 직접 자식만 처리. nested five_SQL DELETE 가 same area 의 record-pointer race 로 첫 매치 후 dbSkip 이 EOF 로 jump (debug 확인). 전체 fix 는 별도 area 강제 또는 child IDs pre-collect 필요. Cross-table cascade 는 정상. |
| LAG/LEAD `default` arg | LAG(v, 1, -1) 의 -1 default 가 무시되고 NIL 반환 — minor. |
### 학습 포인트 (2026-04-24/25)
- **테스트 suite 의 사각지대.** 43/43 이 모두 통과해도 EXISTS-via-semi-join 이 통째로 깨질 수 있음. fix 한 후 같은 카테고리 (NULL 시맨틱 전반) 의 추가 stress 가 필수. 이번 라운드의 EXISTS 버그가 그 사례.
- **Same-table 서브쿼리는 SHARED open 가 필수.** Single-process Five 에서도 두 번째 open 이 필요. EXCLUSIVE 는 자기 자신과 충돌.
- **Five PRG 의 `RETURN-from-SEQUENCE` 가 unwind 안 됨.** 검증 로직은 SEQUENCE 밖에 둬야 함. `Break(NIL)` + RECOVER 패턴은 동작하지만 RETURN 은 구멍.
- **AGG-in-expression 은 두 군데 fix 필요.** ComputeAgg 가 wrapper 를 인식해야 하는 것 외에, hidden-columns 로직도 wrapper 를 descend 해야 함. 둘 중 하나만 fix 하면 silent 0 반환. 43-suite 가 `SUM(amount) AS x` 처럼 wrapper 없는 패턴만 사용해서 잠복. real-world 쿼리의 70% 이상이 `MAX(id)+1` / `COUNT(*)+1` / `ROUND(AVG(p), 2)` 같은 wrapper 사용 — 매우 위험한 silent bug.
---
## 완료 (2026-04-22/23: SQL NULL 라운드 + 대형 런타임/컴파일러 버그)
정확성 중심 라운드. 이번 세션에만 15+ 개의 실제 버그(silent data loss /
semantic 오류) 수정. 3개 게이트 모두 그린 유지.
| 범주 | 항목 | 키 파일 |
|------|------|---------|
| RDD | DBF nullable columns via hidden `_NullFlags` bitmap (Harbour VFP convention). FieldFlagSystem=0x01 / Nullable=0x02 / Binary=0x04. `PutValue` 가 NIL 쓰기 → 비트 세팅, `GetValue` 가 비트 세팅된 행 → NIL 반환. 비-nullable 테이블은 zero overhead (hidden 컬럼 append 안 함) | `hbrdd/dbf/null.go` (new), `header.go`, `dbf.go` |
| DDL | `NOT NULL` / `NULL` / `PRIMARY KEY` 파싱 + Flag 바이트로 저장 | `TSqlDDL.prg` |
| DDL | `ALTER TABLE ADD/DROP COLUMN` 에서 flag 보존 (buffer-all-rows-first migration) | `TSqlDDL.prg:SqlAlterAddColumn/DropColumn` |
| SQL | NOT NULL 런타임 enforce (INSERT + UPDATE) | `TSqlExecutor.prg:RunInsert/RunUpdate` |
| SQL | UNIQUE × NULL: multiple NULLs 허용 (SQL 표준) | `TSqlDDL.prg:SqlValidateUnique` |
| SQL | NULL 3값 논리: `NOT IN (..., NULL)` → NULL, `NOT(NULL) = NULL`, IN 이 NULL 포함 리스트에서 no-match 시 NULL 반환 | `TSqlExecutor.prg:EvalExpr` |
| SQL | Multi-row `INSERT VALUES (...), (...), (...)` + `INSERT ... SELECT` 완성. 이전엔 파서가 첫 튜플만 수용. | `TSqlParser2.prg:ParseInsert`, `TSqlExecutor.prg:RunInsert` |
| SQL | `.T.` / `.F.` / `.Y.` / `.N.` Harbour logical literals + SQL `TRUE`/`FALSE` + `DATE 'YYYY-MM-DD'` | `sqlhelpers.go:lexSQL` (Go), `TSqlLexer.prg` (PRG mirror), `TSqlParser2.prg:ParsePrimary` |
| SQL | INSERT/UPDATE 에서 string → Date 자동 변환 when 타겟 D. UPDATE 는 fast-path 게이트로 강제 PRG | `TSqlDDL.prg:SqlCoerceToCol`, `TSqlExecutor.prg:RunUpdate` |
| SQL | Window `AVG/SUM/COUNT/MIN/MAX OVER ()` (ORDER BY 없음) → **whole-partition aggregate**. 이전엔 running average 반환 (SQL 표준 위반; Postgres/SQL Server 결과와 diverge) | `TSqlExecutor.prg` |
| SQL | `SET DELETED` 누수 fix: `Run()` 진입 시 save/force ON/restore on exit. 이전에 `five_SQL()` 호출이 호출자의 SET DELETED 상태를 조용히 flip | `TSqlExecutor.prg:Run` |
| SQL | DDL 타입 파서의 `D` substring 버그 fix (DOUBLE/DEC/D 충돌로 DATE 컬럼을 N(18,6) 로 만들던 문제) | `TSqlDDL.prg` |
| RTL | `CToD` ISO 포맷 pre-pass: YYYY-MM-DD / YYYY/MM/DD / YYYYMMDD / YYYY.MM.DD | `hbrtl/datetime.go` |
| RTL | `dbStruct()` → 5-element rows (name/type/len/dec/flags). 하위 호환 유지 | `hbrtl/procinfo.go` |
| RTL | `DbCreate` 5th element = Flags byte (optional) | `hbrtl/indexrtl.go` |
| RTL | `SqlBulkInsert``continue on NIL` skip 제거 — CTE/서브쿼리 materialization 에서 NULL 보존 | `hbrtl/sqlscan.go` |
| **런타임** | **Cross-area state-bleed (CRITICAL).** `Thread.waSel` 인터페이스가 `Current() uint16` 요구했으나 `WorkAreaManager.Current()``Area` 반환. 타입 단정이 조용히 실패 → `ALIAS->(expr)` 프리픽스가 silently no-op. 두 번째 워크에리어가 열리자마자 모든 alias-prefix expression 깨짐. `CurrentNum() uint16` 로 교정. ALTER TABLE 다중 행 마이그레이션의 "1 rows migrated" 버그의 근본 원인이었음 | `hbrt/thread.go:WASaveAndSelect*` |
| 컴파일러 | Bare-statement `Set(n, v)` 가 SET 커맨드로 파싱되어 args 가 드롭됨. 다음 토큰이 `(` 이면 expression parser 로 라우팅. 다른 명령 토큰(Skip/Select/Seek)은 expression 연산자를 원래 허용해서 문제 없음 (사인 확인). | `compiler/parser/stmtreg.go:stmtSet` |
| 테스트 | ~80 custom tests 추가 (NULL E2E, NOT NULL, UNIQUE+NULL, multi-row INSERT, cross-area stress, ALTER ADD/DROP, window × NULL, subquery NULL, DATE/LOGICAL literals, string→date, LEFT JOIN × NULL, PARTITION BY × NULL) | `/tmp/test_*.prg` |
### 학습 포인트
- **타입 단정 실패는 silent.** Go 인터페이스 단정이 실패하면 `ok=false` 지만 호출 블록 전체가 스킵되는 경우 표면 증상 없이 "작동하는 것처럼" 보일 수 있음. 프리픽스-alias 버그가 그 예시.
- **"이전에 동작했던 것" 의 함정.** Cross-area 버그를 고치자 `SqlAlterDropColumn` 이 처음으로 깨짐 — 버그에 의존해서 우연히 동작했던 코드가 드러남. 후속 회귀 스트레스가 필수.
- **SQL 기본 프레임은 ORDER BY 유무에 의존.** OVER () 없이 ORDER BY 는 RANGE UNBOUNDED PRECEDING 실행 (running), ORDER BY 없이 OVER() 는 whole partition. Oracle 12c 미만은 틀렸지만 표준은 Postgres / SQL Server 쪽.
---
## 완료 (이번 세션, 실측 기준)
| 계층 | 항목 | 수치 |
|---|---|---|
| RDD | formatNumericField byte writer | **4.1×** (w/ alloc -100%) |
| RDD | CDX leaf slab reuse | **1.6×** (alloc -100%) |
| RDD | parseMemoRef byte parser | **1.38×** |
| RDD | CDX seek zero-copy + seekBuf | alloc -100% per seek |
| RDD | ForFunc compiled closures (INDEX FOR) | **3-5×** 필터 인덱스 |
| RDD | Windows mmap (dbf + ntx + cdx) | **2-4×** on Windows |
| RDD | Mem driver lock-free reads | **5×** @ 8 cores (scales linearly) |
| RDD | Windows `dbf.go` build tag 분리 | 정확성 |
| RDD | PGO 인프라 (`FIVE_CPUPROFILE` + `default.pgo`) | 미래 대비 |
| gengo | `DebugLineFast` + `g.Debug` 게이트 복원 | ~5% tight loop + size 감소 |
| SQL | TSqlIndex per-row executor 재사용 | **1.91×** index scan |
| SQL | ND_COL pre-resolve (`PreResolveColumns`) | 3% JOIN (HAVING/CASE 영향 더 큼) |
| SQL | RunDelete Go RTL (`SqlBulkDelete`) | **2.14×** bulk DELETE |
| SQL | 슬라이딩 윈도우 프레임 (prefix-sum in Go) | **17×** wide-frame window |
| SQL | 멀티-컬럼 ORDER BY index 매치 | 정확성 개선 (perf는 SqlScan LIMIT pushdown 필요) |
| SQL | **SqlScan LIMIT pushdown + TryIndexScan LIMIT** | **770× ORDER+LIMIT, 207× WHERE+ORDER+LIMIT** (see 2026-04-20) |
| RDD | NTX OrderListAdd — store KeyExpr from header | 정확성 (DBOI_EXPRESSION 이 re-open 후 빈 문자열 반환하던 버그) |
| RDD | DBOI_KEYSIZE + OrderKeyLen — expose index key width | 정확성 (BuildKey 의 Str(N,10) 하드코드가 N(8,0) 인덱스 ordScope 무효화) |
| Compiler | `SELECT <bare-ident>` — 리터럴 alias 이름으로 해석 | 정확성 (undefined 식별자 fallback 이 `_wa.Select("")` 로 빈 워크에리어 생성 → SqlAlterAddColumn 이 0 rows migrated — classic Clipper semantics) |
`MakeStringBytes` primitive 는 hbrt/value.go 에 살려 두었지만 실제
hot path 에 배선은 안 되어 있음 (#1 CHAR zero-copy 가 FiveSql2 의
CTE 임시 테이블 수명 위반으로 UAF 유발 → revert). refcounted mmap
인프라가 갖춰지기 전까지 dormant.
---
## 남은 우선순위 (audit 보고서 기준)
### Tier 1 — 큰 구조 개선
1. ~~**A1. JOIN predicate pushdown + hash-join threshold**~~ **DONE (2026-04-20)**
(a) `JoinRecurse``aPushByLevel` 파라미터 추가 + `SplitAndClauses`/`BuildAliasLevelMap`/
`ClauseMaxLevel`/`EvalPushedAtLevel` 헬퍼. `RunSelect``xWhere`를 top-level AND로
분해하고 각 conjunct가 참조하는 alias의 최대 깊이를 계산해 중간 level에 pin된
술어는 해당 level의 매치 직후 평가 → false면 더 깊은 join 재귀 스킵. 부수 발견:
`aJoins[i][3]`이 JOIN 동기화 루프에서 temp alias(`ORD_2`)로 덮어쓰여서 쿼리 본문의
SQL alias(`o`)와 매핑 실패하던 문제 → `aTables` 전체를 스캔하여 원래 테이블명 /
temp alias / 원래 SQL alias 3개 키를 모두 등록. 실측: 100×10×10 = 10k triples에서
mid-level WHERE 기준 **1055ms → 361ms = ~3×**. 깊은 level WHERE는 residual로
fallback (변화 없음). 정확성 테스트 8/8, 3-gate 통과.
(b) `lUseHash` 게이트에 inner RecCount > 64 조건 추가 — 소규모 내측 테이블은
해시 빌드 오버헤드 없이 nested-loop. 기존 43/43 유지.
2. ~~**SqlScan LIMIT pushdown**~~ **DONE (2026-04-20)** — 단일 테이블 LIMIT / ORDER BY-from-index / WHERE + ORDER + LIMIT 경로에서
Go `SqlScan` 과 PRG `TryIndexScan` 양쪽에 조기 종료 훅. 실측 770× / 207×.
부수 발견: NTX `OrderListAdd` 가 키 표현식을 빈 문자열로 저장하던 버그,
`BuildKey` 가 Str(N, 10) 을 하드코드해 N(8,0) 인덱스에서 ordScope 가 무효화되던 버그 동시 수정.
(NTX `Index.KeyExpr()` / `DBOI_KEYSIZE` / `DBFArea.OrderKeyLen(n)` 추가.)
### Tier 2 — 개선 효과 중간
3. ~~**C1 RunUpdate 인덱스 유지 검증**~~ **DONE (2026-04-20)** — 감사 결과: `SqlBulkUpdate`
`DBFArea.PutValue`(바이트 쓰기 + dirty flag) 경유하며 인덱스 키 del/add 없음. 더 큰 갭:
`dbf/indexer.go` 어디에도 `ordKeyDel` / `ordKeyAdd` 훅이 없어 `Append` / `PutValue` / `Delete`
전반이 인덱스를 유지하지 않음. 단, **SQL 엔진의 `SqlExecOpenTable`이 .ntx를 열지 않으므로**
SQL-only 경로에서는 증상이 안 보이고, 호출자가 `SET INDEX TO`로 같은 워크에리어를 미리
연 경우에만 divergence가 재현됨. 수정: `SqlBulkUpdate` 말미에 "업데이트된 필드가 열린
인덱스의 KeyExpr에 등장하면 `OrderListRebuild()`" 가드 추가 (substring 매치로 보수적 판정,
인덱스 없는 테이블 / 미커버 필드는 no-op → B13 48× 경로 영향 0). 회귀 테스트 4/4 통과.
**Follow-up**: `Append` / `FieldPut` 경로 per-record maintenance, `SqlExecOpenTable`
`.ntx`/`.cdx`를 자동 attach하는 설계는 별도 세션.
4. ~~**C6 / C7 스키마 버전 키 기반 plan-cache invalidation**~~ **DONE (2026-04-20)**`s_nSchemaVer`
STATIC 카운터를 TSqlExecutor.prg 에 추가, 모든 DDL 메서드 (CreateTable/Index/View,
DropTable/Index/View, AlterTable ADD/DROP) 성공 시 `SqlBumpSchemaVer()`. TFiveSQL
에서 캐시 키를 `hb_NToS(SqlSchemaVer()) + "|" + cKey` 로 생성 → s_hPlanCache 와
s_hDmlPcodeCache (cCacheKey 통해) 양쪽 자동 무효화. 회귀 테스트 9/9 통과.
5. ~~**A3 보완: MIN/MAX 슬라이딩 윈도우 (monotonic deque)**~~ **DONE (2026-04-20)**
`SqlWindowSlideAgg`에 monotonic deque 분기 추가 (numeric only, 비-numeric 값
감지 시 `.F.` 반환하여 PRG O(N·W) fallback). 부수 발견: `SqlExprHasAgg`
ND_WINDOW를 의도적으로 descend 안 해서 숨겨진-컬럼 로직이 아예 발동 안 되어
왔음 → `SELECT id, SUM(v) OVER (…)` 같은 쿼리는 SALARY가 SELECT 리스트에
없으면 모든 행 NIL 반환하던 버그 동시 수정 (TSqlExecutor.prg의 hidden-column
루프에서 `.OR. aCols[i][1][1] == ND_WINDOW` 추가). 실측: 5000행 × W=201
wide frame MIN/MAX **1427ms → 22ms = 65×**. 회귀 테스트 27/27, 3-gate 통과.
### Tier 3 — smaller wins
6. ~~**A4 UNION 스트리밍 DISTINCT**~~ **DONE (2026-04-21)** — Go RTL `SqlUnionDistinct(aL, aR)`
추가: 단일 패스로 `aL + unique(aR)` 생성. 기존 경로는 `aRows ++= aU[2]; SqlDistinct(aRows)`
— 두 번 순회 + 중간 머지 배열 할당. 실측: 5k+5k 50% overlap UNION 19→17ms (~11%), 4-way
UNION 34→24ms (~29%). UNION ALL은 no-op. Big-O 동일하지만 할당/패스 감소.
7. ~~**A5 상관 subquery cache key 재구성**~~ **DONE (2026-04-21)** — Go RTL
`SqlBuildSubCacheKey(nId, aValues)` 추가. 기존 PRG 루프는 `hb_ntos(nId) + "@" +
SqlValToStr(v1) + "|" + ...` 방식으로 N+1 문자열 할당 + N SqlValToStr PRG 호출.
새 경로: PRG는 free-var 값을 배열로 모아 Go RTL 한 번 호출 (canonical
appendValueHashKey 포맷으로 join). 캐시 히트 시 outer row당 N회 allocate가
O(1)로 감소. FiveSql2 43/43 유지 (별도 상관 subquery 벤치 없음, 기능적 중립).
8. ~~**C2 RunDelete 제약 검증 (CHECK + FK)**~~ **DONE (2026-04-21)** — DDL 파서에
`ON DELETE CASCADE/RESTRICT/SET NULL/NO ACTION` 수용 + .fsc 4번째 필드로 저장
(backward-compatible: 4번째 필드 없으면 RESTRICT). `SqlFindReferencingFKs`
디렉터리의 모든 .fsc를 스캔해 이 테이블을 참조하는 FK들을 수집. `RunDelete`
referencing FK가 있으면 fast-path(SqlBulkDelete) 우회하고 PRG 경로에서
`SqlEnforceDeleteRefs`를 통해 per-record 집행: RESTRICT은 child에 매칭 행이
있으면 에러 반환 / CASCADE는 중첩 `DELETE` / SETNULL은 중첩 `UPDATE ... = NULL`.
회귀 테스트 9/9 (RESTRICT default, CASCADE 전파, SET NULL 유지, 명시 RESTRICT).
주의: 중첩 five_SQL 호출이 워크에리어를 옮기므로 부모 RecNo/Select를 enforce
호출 전후에 저장·복원해야 dbDelete/dbRUnlock이 맞는 행에 적용됨 — 첫 구현에서
놓쳐 첫 row에서 enforce 자체를 우회하는 버그 발생, 디버그 후 수정.
### gengo 리뷰의 low-effort fix 들
9. ~~**`emitSwitch` 타입 강제 수정**~~ **DONE (2026-04-21)** — 기존 emit는
`_sw.AsNumInt() == caseVal.AsNumInt()` 로 모든 CASE를 비교했음 → "ABC".AsNumInt() = 0
이라 모든 non-numeric SWITCH가 첫 CASE로 고정. Fix: 런타임 `t.Equal()` 경유 (타입 인식
비교, CHAR trim / numeric promotion / date Julian 규칙 보존). 각 CASE를 독립된
`if !_swHit { ... }` 블록으로 emit하여 스택 균형 + EXIT/RETURN 내성. 검증:
string/numeric/logical SWITCH 정상 매치. (날짜 CASE 실패는 `CToD` 파서의 별개 버그.)
10. ~~**`emitPostfixExpr` 식 컨텍스트 수정**~~ **DONE (2026-04-21)** — 기존 코드는
`Dup + Inc + Pop` 로 스택 상단 사본만 증가시키고 변수 저장소는 건드리지 않아,
`y := x++` 이 y는 old x로 정답이지만 x는 그대로였음. LOCAL / STATIC / self-field
3가지 타겟별로 (a) 현재 값 push (b) 저장소 직접 업데이트 — LOCAL은 `LocalAddInt`,
STATIC은 `_v := goVar.AsNumInt() + δ; goVar = hbrt.MakeInt(_v)`, self-field는
`PushSelfField + Plus/Minus + SetSelfField`. 회귀: `y := x++` (5→6, y=5),
`y := x--` (10→9, y=10), `y := x++ + x++` (x 0→2, y=1) 모두 정답.
11. ~~**`emitForEach` hash / string iteration**~~ **DONE (2026-04-21)** — 기존 emit는
배열 케이스만 처리하고 hash/string은 조용히 skip. 3-way dispatch 추가: IsArray /
IsHash (Values 슬라이스 순회, 삽입 순서) / IsString (바이트 단위 1-char 서브스트링).
회귀: 배열 합 60, 해시 합 6, 문자열 `abc``a-b-c-` 모두 정답.
12. ~~**gengo.go 3545 라인 분할**~~ **PARTIAL (2026-04-21)** — 3640→1629 줄 (55% 감소):
`folding.go` (456줄, constant folding + const-local propagation),
`emit_stmt.go` (1351줄, emitStmt 및 모든 statement emitter), `emit_block.go`
(287줄, emitAliasExpr / emitSendExpr / emitBlock + closure walker). 남은 gengo.go는
core Generator API와 expr / RTL-inline / 헤더 방출. 추가 분할 (emit_expr.go,
emit_rtl_inline.go)은 가치 대비 리스크 낮아 생략 — 현 상태로도 파일당 300-1600줄이
관리 가능 범위. Go ALL / FiveSql2 43/43 / compat 56/56 유지.
---
## 추천 진행 순서 (다음 세션)
1. **SqlScan LIMIT pushdown** (0.5 일) — A2 를 비로소 체감 가능하게 만듦. 진입장벽 낮음.
2. **C6 / C7 schema-version cache** (0.5 일) — 정확성 핵심. DDL-heavy 워크로드 전에.
3. **C1 SqlBulkUpdate 인덱스 감사** (0.5 일) — 잠재 correctness bug 검증.
4. **A3 MIN/MAX deque** (1 일) — wide-frame 완결.
5. **A1 JOIN predicate pushdown** (2 일) — 마지막 큰 개선.
Tier 3 (A4 / A5 / gengo fix 들) 은 시간 남으면.
---
## 향후 기능 로드맵 (2026-04-23 정리)
correctness 라운드(SQL NULL + 대형 버그 감사)가 마무리된 시점의 새 기능
후보 목록. 각 항목에 크기·가치·구현 노트를 기재. 우선순위는 "사용자가
실제로 쓸 쿼리에 얼마나 자주 걸리는가" 기준.
### Tier A — 많이 쓰는 SQL 표준 기능 (중간 크기)
- [ ] **A-1 MERGE / UPSERT 완결 검증**
- 크기: 0.51 일
- 현 상태: test_sql1999.prg 섹션 5 (5a/5b/5c) 에 MERGE 테스트가 있음.
최근 중간 라운드에서 한 번 깨졌다가 돌아옴 → 현재 경로에 잠재된
edge case 가 있을 가능성.
- 할 일: `INSERT ... ON CONFLICT DO UPDATE` / `ON CONFLICT DO NOTHING`
구문 지원 검토 (MERGE 와 별개로 Postgres 스타일이 사용자 친화적).
현재 MERGE 는 SQL:2003 순수 구문.
- 노트: 충돌 감지는 UNIQUE constraint 기반. `.fsc` 에 이미 있으니 재사용.
- [ ] **A-2 INTERSECT / EXCEPT**
- 크기: 0.5 일
- UNION / UNION ALL 은 구현됨. INTERSECT (양쪽 공통) / EXCEPT (왼쪽 오른쪽)
가 미확인.
- 구현: 기존 `SqlUnionDistinct` 패턴 재사용. Go RTL `SqlIntersect` /
`SqlExcept` 추가하고 파서에서 키워드 라우팅.
- 표준: DISTINCT 시맨틱이 기본. `INTERSECT ALL` / `EXCEPT ALL` 은 멀티셋
버전 (행별 카운트 유지) — 원한다면 optional.
- [ ] **A-3 FULL OUTER JOIN**
- 크기: 0.51 일
- LEFT/RIGHT/INNER 있음. FULL 은 "양쪽 모두 매칭 없어도 행 반환".
- 구현: `JoinRecurse` 에 FULL 분기 추가. LEFT 로 한 번 스캔 후 오른쪽에서
매칭 안 된 행을 outer-emit. 중복 안 되게 매칭 인덱스 플래그 필요.
- 주의: TryGoJoin 게이트는 이미 LEFT/RIGHT/FULL 을 PRG 로 fallback 시키고
있음 — PRG 경로만 건드리면 됨.
- [ ] **A-4 CAST / type conversion coverage**
- 크기: 0.5 일
- `CAST(expr AS type)` 이 기본은 동작 (ND_FN "CAST"). 실무 케이스:
CHAR↔NUMERIC, DATE↔CHAR, NUMERIC(18,4) 같은 width/precision 변환.
- 할 일: 각 (from-type, to-type) 매트릭스 테스트 작성 → 빠진 path 발견.
특히 오늘 고친 string→date 자동 coercion 의 명시적 버전.
- [ ] **A-5 추가 window 함수**
- 크기: 1 일
- 있는 것: ROW_NUMBER / RANK / DENSE_RANK / LAG / LEAD / SUM/AVG/COUNT/MIN/MAX.
- 추가 대상: **FIRST_VALUE** / **LAST_VALUE** / **NTH_VALUE** / **NTILE(n)** /
**CUME_DIST** / **PERCENT_RANK**.
- 구현: 기존 partition-sort 인프라 재사용. FIRST_VALUE/LAST_VALUE 는 frame
경계만 보고 O(1) 에 쓸 수 있음. NTILE/CUME_DIST/PERCENT_RANK 는 partition
크기 + rank 만 있으면 됨.
- 주의: 오늘 고친 "no ORDER BY → whole partition" 시맨틱이 FIRST/LAST_VALUE
에 중요 (frame 의미가 달라짐).
### Tier B — 개발 편의 / 디버깅 (작음)
- [ ] **B-1 EXPLAIN**
- 크기: 0.5 일
- `EXPLAIN SELECT ...` → plan tree 를 JSON / 들여쓰기 텍스트로 덤프.
- 구현: plan cache 의 `hQuery` 해시를 recursive-walk 하는 Go RTL. 각
노드에 type / alias / args 표시.
- 가치: 쿼리 튜닝 시 어떤 경로 (TryGoJoin 해시 / JoinRecurse nested loop
/ index scan / table scan) 를 타는지 즉시 파악 가능. 지금은 `FIVE_SQL_TRACE`
env 같은 게 없으면 찍어봐야 알 수 있음.
- [ ] **B-2 INDEX ON expression 검증**
- 크기: 0.5 일
- `CREATE INDEX idx ON t (UPPER(name))` 같은 함수 호출을 키식으로 받는지 확인.
- 현재 DDL 은 `CREATE INDEX ... ON ... (col_list)` 만 파싱할 가능성 큼.
- 할 일: 파서 확장 + 인덱스 키 식을 표현식 트리로 저장. Harbour RDD 는
`INDEX ON <expr> TO <file>` 로 이미 임의 표현식 허용하니 SQL 쪽 DDL 게이트만
풀면 됨.
- [ ] **B-3 GREATEST / LEAST**
- 크기: 0.5 일
- `GREATEST(a, b, c)` / `LEAST(a, b, c)` — 여러 인자 중 max/min. SQL 표준
(Oracle/Postgres). NULL-aware: Postgres 는 NULL 무시, Oracle 은 어느
하나라도 NULL 이면 NULL. Postgres 쪽을 추천.
- 구현: ND_FN 에 케이스 추가. 단순 reduce.
- [ ] **B-4 String 함수 coverage 스캔**
- 크기: 0.25 일
- SUBSTRING / POSITION / LENGTH / CHAR_LENGTH / TRIM / UPPER / LOWER /
REVERSE / LPAD / RPAD / CONCAT / REPLACE / LIKE — 현재 구현 매트릭스
감사. 실무에서 기대하는데 없어서 당황스러운 것 우선.
- Harbour RTL 에 이미 상당수 있음 → 파서/라우터에서 매핑만 하면 됨.
### Tier C — 구조적/큰 기능 (큰 크기)
- [ ] **C-1 Triggers (BEFORE/AFTER INSERT/UPDATE/DELETE)**
- 크기: 23 일
- `.fsc` 에 트리거 바디(SQL 텍스트) 저장. DDL 에 `CREATE TRIGGER ...` 파서.
DML 경로 (RunInsert / RunUpdate / RunDelete) 에서 matching 트리거 실행.
- 주의: 트리거 내 `NEW.col` / `OLD.col` 참조 지원 → 로컬 컨텍스트에 row
스냅샷 push 해야 함. 무한 재귀 가드 (DEPTH 제한).
- [ ] **C-2 Generated / Computed columns**
- 크기: 1 일
- `col N(10,2) AS (price * quantity) VIRTUAL`
- VIRTUAL: 쿼리 시 계산. STORED: write 시 계산해 실제 저장.
- 간단한 가상 컬럼은 SELECT 리스트 치환으로 구현 가능.
- [ ] **C-3 JSON 타입 + 함수**
- 크기: 23 일
- 컬럼 타입 "J" 로 JSON 문자열 저장. `JSON_EXTRACT(col, '$.path')` /
`JSON_SET` / `JSON_ARRAY` / `JSON_OBJECT`.
- 구현 포인트: JSON 파서 + path 평가 엔진. Go encoding/json 으로 배킹하면
빠르지만 path 평가기 직접 작성 필요.
- [ ] **C-4 Prepared statement API**
- 크기: 1 일
- SQL `PREPARE stmt FROM '...'` / `EXECUTE stmt USING ...` / `DEALLOCATE stmt`.
- 현재 `?` 파라미터 + `TFiveSQL:ExecuteWith` 는 있음 → 명시적 PREPARE 구문
만 얹으면 됨. 장점: 바인딩 계약이 SQL 레벨에서 명확해져서 디버깅 용이.
### Tier D — 엔터프라이즈 / 고급 (매우 큼)
- [ ] **D-1 Full-text search**`MATCH(col) AGAINST('keywords')`. 별도 역색인.
- [ ] **D-2 SQL stored procedures** — PRG 로 이미 쓸 수 있지만 SQL 구문 `CREATE PROCEDURE`.
- [ ] **D-3 Row-level security (RLS) / policies**.
- [ ] **D-4 Temporal queries**`FOR SYSTEM_TIME AS OF ...`, history table.
- [ ] **D-5 Replication / 복제** — WAL-기반 mirror.
- [ ] **D-6 Clustered / covering indexes** — 인덱스 리프에 추가 컬럼 저장.
### 추천 실행 순서 (상위 우선순위만)
1. **A-2 INTERSECT/EXCEPT****A-3 FULL OUTER JOIN****A-5 window 함수 추가**
**B-1 EXPLAIN****A-1 MERGE 재검증**.
→ 하루 2개 페이스로 A+B 세트 ~1주 분량.
2. **C-1 Triggers** 는 스테이트풀/재귀 리스크 큼. 별도 브랜치에서 스파이크 먼저.
3. Tier D 는 특정 고객/워크로드 요구 있을 때만.
### 기능 추가 전 체크리스트
- 테스트 PRG 먼저 (`/tmp/test_<feature>.prg`) — 기대 동작을 Assert 로 못박기.
- 3 게이트 매 번 돌리기 (Go unit / FiveSql2 43/43 / compat 56/56).
- 커스텀 PRG 테스트를 `_FiveSql2/test/` 에 접수해 43/43 → 44/44, 45/45... 로
성장시키기. 오늘까지 발견된 버그들 상당수가 43 개 테스트 범위 밖이었음 —
커버리지 확장이 정확성에 직접 기여.
---
---
## 학습한 교훈 (다음 세션에서도 유효)
1. **감사 보고서 맹신 금물** — 이론적 안전성 ≠ 실제 FiveSql2 워크로드 안전성.
예: #1 CHAR zero-copy 는 "mmap 은 Close 까지 유효" contract 였으나 CTE 임시 테이블이
쿼리 중 Close 반복 → UAF SIGSEGV. Revert + primitive 보존.
2. **항상 A/B 실측** — 감사 예측 대비 실측이 크게 어긋나는 케이스 많음.
예: SQL L1 ND_COL pre-resolve 는 15-30% 예측했으나 실제 3% (WHERE 가 PcCompile
로 이미 처리되어 EvalExpr 우회).
예: A2 는 SqlScan 이 LIMIT pushdown 미지원이라 측정 불가.
3. **여러 경로가 있을 때 hot path 가 우리 타겟인지 먼저 확인**#10 Mem lock-free
는 단일 스레드 벤치에서는 작은 차이, 실전 SHARED 에서는 코어 수 선형 확장.
4. **정확성은 성능보다 우선** — CLAUDE.md 절대 규칙: 하나라도 테스트 실패 시 즉시 revert.
5. **큰 파일 편집 시 LOCAL 변수 선언 주의** — 메서드 끝에 선언된 LOCAL 과 블록 안 inline
LOCAL 은 Five 파서에서 주의 필요. 최소 재현 테스트로 확인.
---
## 세션 종료 시 상태
- 작업 디렉토리: `/Users/charleskwon/Projects/fivedev/five`
- 전 테스트 통과. 빌드 클린.
- `default.pgo` 프로파일 파일 존재 (bench_bulk 기반).
- 벤치 / 테스트 PRG 흔적 `/tmp/` 에: `test_sql`, `test_compat`,
`bench_bulk`, `bench_idx`, `bench_del`, `bench_win`, `bench_order_*`,
`check_order`, `test_deep_err`, `test_dbg*`.
- `~/tmp/error.log` 이전 에러 덤프 보관.
---
## 완료 (2026-04-30: PP / std.ch 라운드 — 9 commits)
파서가 silent no-op 으로 삼키던 xBase 명령들을 Harbour-style `#command`
규칙으로 옮겼다. 결과: 13개 명령이 시즌 처음으로 *실제* 동작한다.
PP 자체에서는 14건의 결함을 추가로 잡았다.
### 동작하기 시작한 명령들
| 명령 | RTL 백엔드 | 비고 |
|------|-----------|-----|
| `ERASE`/`DELETE FILE`/`RENAME` | `FErase`/`FRename` | 이전엔 silent no-op (파일 안 지움) |
| `CLOSE`/`CLOSE ALL`/`CLOSE DATABASES`/`CLOSE <alias>` | `DbCloseArea`/`DbCloseAll` + alias-arrow | 알리아스 형은 알리아스 무시했었음 |
| `COMMIT`/`UNLOCK`/`UNLOCK ALL` | `DbCommit`/`DbRUnlock`/`DbUnlock` | |
| `LOCATE`/`CONTINUE` | `__dbLocate`/`__dbContinue` | 기존 RTL — 파서 하드코드만 제거 |
| `REINDEX`/`PACK`/`ZAP` | `DbReindex`/`DbPack`/`DbZap` | |
| `KEYBOARD`/`RUN` | `Keyboard`/`hb_Run` | |
| `COUNT`/`SUM`/`AVERAGE` | `dbEval` (`__dbAverage` 만 신규) | Harbour 그대로 dbEval 위에 쌓는 매크로 |
| `COPY TO` `[FIELDS] [FOR/WHILE/NEXT/REC/REST/ALL]` | 신규 `__dbCopy` | DBF→DBF (SDF/DELIMITED 미지원) |
| `SORT TO` `[ON keys/D] [FOR/...]` | 신규 `__dbSort` | 다중 키, `/D` desc, stable insertion sort |
| `LIST`/`DISPLAY` `[fields] [OFF] [FOR/...]` | 신규 `__dbList` | OFF/ALL 차이는 Harbour 그대로 |
| `TOTAL TO` `ON <key> [FIELDS]` | 신규 `__dbTotal` | 연속 동일-키 그룹별 합. memo 필드 제외 |
| `JOIN WITH <alias> TO <f>` | 신규 `__dbJoin` | nested-loop, master-precedence 필드 union |
| `UPDATE FROM <alias> [ON <key>] [RANDOM] REPLACE ...` | 신규 `__dbUpdate` | `_FIELD-><key>` wrapping 으로 dispatch |
파서의 IDENT-statement no-op switch 에서 16개 키워드 제거 — `parseIdentStmt`/`parseExprStmt` 두 곳 동기화된 상태로.
### PP 자체 결함 fix 14건
1. partial-pattern false-match (literal tail 미검사) — `CLOSE``CLOSE ALL` 룰에 매치
2. 미캡쳐 marker tail 우선순위 (`<a>` 가 옵션이 아닌데 옵션처럼 skip) — `CLOSE``CLOSE <a>` 룰에 매치
3. `<{name}>` blockify substitution 미구현
4. `findMarkerEnd``{`/`}` prefix/suffix 인식 못함
5. `<(name)>` 패턴 marker 의 capture 키에 괄호 baked-in (smart-stringify 안 동작)
6. 옵션 절 marker 가 outer pattern 끝까지 greedy 캡처 — `[TO <(f)>] [FOR <for>]` 에서 file 이 FOR 까지 삼킴
7. `matchSegment` 가 옵션 절 내 `MarkerList`(`<fields,...>`) 미지원
8. `captureExpression` 이 첫 delimiter 만 사용 — 옵션 절 chain 에서 모든 후속 키워드 stop 못함
9. `<(name)>` smart-stringify 가 list capture 에 element-별 quote 안 함 — `{ "a , b" }` ←→ `{ "a", "b" }`
10. `<{name}>` blockify 가 list capture 에 element-별 wrap 안 함 — `{|| a , b }` ←→ `{ {|| a }, {|| b } }`
11. `#command` 다중-행 line-continuation `;` 미지원 — std.ch 형식의 멀티라인 룰 자체가 등록 안 됨
12. `matchSegment``MarkerWordList` (`<off:OFF>`) 미지원
13. List/regular capture stop 경계가 `MarkerWordList` 의 값들을 인식 못함 — `[<v,...>] [<off:OFF>]` 에서 v 가 OFF 까지 삼킴
14. 옵션-반복 loop 가 no-progress iteration 의 빈 capture 를 `\x01` 로 contaminating 후 break — multi-capture mode 로 잘못 들어감
15. 미캡쳐 `<.name.>` 이 빈 문자열로 정리됨 (Harbour idiom: `.F.` 가 정답)
16. `matchSegment` 가 nested `[...]` 옵션 절 미지원 — `[REPLACE <f1> WITH <x1> [, <fN> WITH <xN>]]` 에서 inner `[` 를 literal 로 매치 시도
### 누적 silent 버그 fix
PP/std.ch 라운드 14건 + 시즌 전체 (~62 + 14) = **약 76건**.
---
## 보류 — 아주 먼 미래 (LABEL / REPORT)
xBase 의 `LABEL FORM` / `REPORT FORM` 명령은 별도 `.lbl` / `.frm` 바이너리
포맷 파일을 읽어 페이지/컬럼 폭/머리·꼬리/그룹 break 등을 처리하는
**완전한 출력 포맷팅 엔진**이다.
* 사용 빈도: 1990년대 dBASE/Clipper 환경에서나 흔했고, 현 시점 신규 코드에서
거의 안 쓰임. SQL + 외부 PDF/HTML 라이브러리로 대체된 지 오래.
* 구현 비용: `.lbl`/`.frm` 바이너리 파서 + 페이지네이션/컬럼 폭 계산 +
group break + summary 등 — 예상 ~800~1500 LOC.
* ROI: 매우 낮음. 현존 사용자 거의 없음.
**결정**: 보류. 향후 실수요 사례가 등장하기 전까지는 파서가 silent no-op
으로 삼키는 현 상태 유지. 파서 IDENT-stmt switch 에 `LABEL`/`REPORT`
남아있고, 두 키워드 다음 토큰을 EOL 까지 소비한다 — Harbour 호환 PRG
파일이 컴파일은 되며 (실행하면 무동작) 다른 동작에 영향 없음.
재개 시 진입 지점:
* harbour-core/include/std.ch — `#command LABEL FORM`, `#command REPORT FORM`
* harbour-core/src/rdd/dblabel.prg, dbreport.prg, dbrlist.prg
* harbour-core/src/rtl/frlabel.prg (label .lbl reader)
* `.frm` / `.lbl` 바이너리 포맷: dBASE III 시대 문서 (Sybex 등 옛 reference 자료)
다음 세션 시작 시 `CLAUDE.md` 로드 → 이 파일 읽기 → 위 순서 1번부터 진행.