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>
635 lines
56 KiB
Markdown
635 lines
56 KiB
Markdown
# 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.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 RTL `SqlIntersect` /
|
||
`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_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)**
|
||
- 크기: 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** — 인덱스 리프에 추가 컬럼 저장.
|
||
|
||
### 추천 실행 순서 (상위 우선순위만)
|
||
|
||
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번부터 진행.
|