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

56 KiB
Raw Blame History

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.

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:RunMergematch_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 RETURNBEGIN SEQUENCE 안에서 unwind 안 되는 Five 동작 SqlAlterAddColumn / SqlAlterDropColumn 에서 검증을 SEQUENCE 밖으로 이동
EXISTS regression 위 LIMIT 0 fix 의 부작용으로 ExistsViaSemiJoinhLifted["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". MaterializeRecursiveCTEaTables[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 변환 해서 우연히 동작. EvalExprSqlEvalRowExpr 둘 다 +/- 분기에 (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 가 산술/
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]) > 1OutErr() 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_") 로 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 valND_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*.ntxc_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 SqlBulkInsertcontinue 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) JoinRecurseaPushByLevel 파라미터 추가 + SplitAndClauses/BuildAliasLevelMap/ ClauseMaxLevel/EvalPushedAtLevel 헬퍼. RunSelectxWhere를 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 — 개선 효과 중간

  1. C1 RunUpdate 인덱스 유지 검증 DONE (2026-04-20) — 감사 결과: SqlBulkUpdateDBFArea.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하는 설계는 별도 세션.

  2. 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 통과.

  3. 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

  1. 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 동일하지만 할당/패스 감소.
  2. 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 벤치 없음, 기능적 중립).
  3. 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 들

  1. 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 파서의 별개 버그.)
  2. 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) 모두 정답.
  3. emitForEach hash / string iteration DONE (2026-04-21) — 기존 emit는 배열 케이스만 처리하고 hash/string은 조용히 skip. 3-way dispatch 추가: IsArray / IsHash (Values 슬라이스 순회, 삽입 순서) / IsString (바이트 단위 1-char 서브스트링). 회귀: 배열 합 60, 해시 합 6, 문자열 abca-b-c- 모두 정답.
  4. 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 searchMATCH(col) AGAINST('keywords'). 별도 역색인.
  • D-2 SQL stored procedures — PRG 로 이미 쓸 수 있지만 SQL 구문 CREATE PROCEDURE.
  • D-3 Row-level security (RLS) / policies.
  • D-4 Temporal queriesFOR SYSTEM_TIME AS OF ..., history table.
  • D-5 Replication / 복제 — WAL-기반 mirror.
  • D-6 Clustered / covering indexes — 인덱스 리프에 추가 컬럼 저장.

추천 실행 순서 (상위 우선순위만)

  1. A-2 INTERSECT/EXCEPTA-3 FULL OUTER JOINA-5 window 함수 추가B-1 EXPLAINA-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 미검사) — CLOSECLOSE ALL 룰에 매치
  2. 미캡쳐 marker tail 우선순위 (<a> 가 옵션이 아닌데 옵션처럼 skip) — CLOSECLOSE <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. matchSegmentMarkerWordList (<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 등 — 예상 8001500 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번부터 진행.