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>
56 KiB
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: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 가 산술/ |
|
| 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_") 로 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 — 큰 구조 개선
-
A1. JOIN predicate pushdown + hash-join thresholdDONE (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 유지. -
SqlScan LIMIT pushdownDONE (2026-04-20) — 단일 테이블 LIMIT / ORDER BY-from-index / WHERE + ORDER + LIMIT 경로에서 GoSqlScan과 PRGTryIndexScan양쪽에 조기 종료 훅. 실측 770× / 207×. 부수 발견: NTXOrderListAdd가 키 표현식을 빈 문자열로 저장하던 버그,BuildKey가 Str(N, 10) 을 하드코드해 N(8,0) 인덱스에서 ordScope 가 무효화되던 버그 동시 수정. (NTXIndex.KeyExpr()/DBOI_KEYSIZE/DBFArea.OrderKeyLen(n)추가.)
Tier 2 — 개선 효과 중간
-
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하는 설계는 별도 세션. -
C6 / C7 스키마 버전 키 기반 plan-cache invalidationDONE (2026-04-20) —s_nSchemaVerSTATIC 카운터를 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 통과. -
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
A4 UNION 스트리밍 DISTINCTDONE (2026-04-21) — Go RTLSqlUnionDistinct(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 동일하지만 할당/패스 감소.A5 상관 subquery cache key 재구성DONE (2026-04-21) — Go RTLSqlBuildSubCacheKey(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 벤치 없음, 기능적 중립).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 들
DONE (2026-04-21) — 기존 emit는emitSwitch타입 강제 수정_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파서의 별개 버그.)DONE (2026-04-21) — 기존 코드는emitPostfixExpr식 컨텍스트 수정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) 모두 정답.DONE (2026-04-21) — 기존 emit는 배열 케이스만 처리하고 hash/string은 조용히 skip. 3-way dispatch 추가: IsArray / IsHash (Values 슬라이스 순회, 삽입 순서) / IsString (바이트 단위 1-char 서브스트링). 회귀: 배열 합 60, 해시 합 6, 문자열emitForEachhash / string iterationabc→a-b-c-모두 정답.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 유지.
추천 진행 순서 (다음 세션)
- SqlScan LIMIT pushdown (0.5 일) — A2 를 비로소 체감 가능하게 만듦. 진입장벽 낮음.
- C6 / C7 schema-version cache (0.5 일) — 정확성 핵심. DDL-heavy 워크로드 전에.
- C1 SqlBulkUpdate 인덱스 감사 (0.5 일) — 잠재 correctness bug 검증.
- A3 MIN/MAX deque (1 일) — wide-frame 완결.
- 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.5–1 일
- 현 상태: 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 RTLSqlIntersect/SqlExcept추가하고 파서에서 키워드 라우팅. - 표준: DISTINCT 시맨틱이 기본.
INTERSECT ALL/EXCEPT ALL은 멀티셋 버전 (행별 카운트 유지) — 원한다면 optional.
-
A-3 FULL OUTER JOIN
- 크기: 0.5–1 일
- 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_TRACEenv 같은 게 없으면 찍어봐야 알 수 있음.
-
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)
- 크기: 2–3 일
.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 타입 + 함수
- 크기: 2–3 일
- 컬럼 타입 "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 — 인덱스 리프에 추가 컬럼 저장.
추천 실행 순서 (상위 우선순위만)
- A-2 INTERSECT/EXCEPT → A-3 FULL OUTER JOIN → A-5 window 함수 추가 → B-1 EXPLAIN → A-1 MERGE 재검증. → 하루 2개 페이스로 A+B 세트 ~1주 분량.
- C-1 Triggers 는 스테이트풀/재귀 리스크 큼. 별도 브랜치에서 스파이크 먼저.
- 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 개 테스트 범위 밖이었음 — 커버리지 확장이 정확성에 직접 기여.
학습한 교훈 (다음 세션에서도 유효)
-
감사 보고서 맹신 금물 — 이론적 안전성 ≠ 실제 FiveSql2 워크로드 안전성. 예: #1 CHAR zero-copy 는 "mmap 은 Close 까지 유효" contract 였으나 CTE 임시 테이블이 쿼리 중 Close 반복 → UAF SIGSEGV. Revert + primitive 보존.
-
항상 A/B 실측 — 감사 예측 대비 실측이 크게 어긋나는 케이스 많음. 예: SQL L1 ND_COL pre-resolve 는 15-30% 예측했으나 실제 3% (WHERE 가 PcCompile 로 이미 처리되어 EvalExpr 우회). 예: A2 는 SqlScan 이 LIMIT pushdown 미지원이라 측정 불가.
-
여러 경로가 있을 때 hot path 가 우리 타겟인지 먼저 확인 — #10 Mem lock-free 는 단일 스레드 벤치에서는 작은 차이, 실전 SHARED 에서는 코어 수 선형 확장.
-
정확성은 성능보다 우선 — CLAUDE.md 절대 규칙: 하나라도 테스트 실패 시 즉시 revert.
-
큰 파일 편집 시 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건
- partial-pattern false-match (literal tail 미검사) —
CLOSE가CLOSE ALL룰에 매치 - 미캡쳐 marker tail 우선순위 (
<a>가 옵션이 아닌데 옵션처럼 skip) —CLOSE가CLOSE <a>룰에 매치 <{name}>blockify substitution 미구현findMarkerEnd가{/}prefix/suffix 인식 못함<(name)>패턴 marker 의 capture 키에 괄호 baked-in (smart-stringify 안 동작)- 옵션 절 marker 가 outer pattern 끝까지 greedy 캡처 —
[TO <(f)>] [FOR <for>]에서 file 이 FOR 까지 삼킴 matchSegment가 옵션 절 내MarkerList(<fields,...>) 미지원captureExpression이 첫 delimiter 만 사용 — 옵션 절 chain 에서 모든 후속 키워드 stop 못함<(name)>smart-stringify 가 list capture 에 element-별 quote 안 함 —{ "a , b" }←→{ "a", "b" }<{name}>blockify 가 list capture 에 element-별 wrap 안 함 —{|| a , b }←→{ {|| a }, {|| b } }#command다중-행 line-continuation;미지원 — std.ch 형식의 멀티라인 룰 자체가 등록 안 됨matchSegment가MarkerWordList(<off:OFF>) 미지원- List/regular capture stop 경계가
MarkerWordList의 값들을 인식 못함 —[<v,...>] [<off:OFF>]에서 v 가 OFF 까지 삼킴 - 옵션-반복 loop 가 no-progress iteration 의 빈 capture 를
\x01로 contaminating 후 break — multi-capture mode 로 잘못 들어감 - 미캡쳐
<.name.>이 빈 문자열로 정리됨 (Harbour idiom:.F.가 정답) 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번부터 진행.