From 4d5621c21a662fb59ec831c5c81df5a269309c48 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Mon, 13 Apr 2026 22:58:09 +0900 Subject: [PATCH] feat: CDX compound index write + {||} parsing + zero known constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 3 remaining known constraints resolved. CLAUDE.md now shows zero. 1. CDX compound index WRITE support (was read-only) New file: hbrdd/cdx/build.go (~400 LOC) - CreateOrAddTag() builds Harbour-compatible CDX files - Bit-packed leaf pages (RecBits/DupBits/TrlBits compression) - Interior nodes with big-endian RecNo/ChildPage - Compound root directory (structural B-tree of tag names) - Append-safe: preserves existing tags when adding new ones - Linked leaf pages (LeftPtr/RightPtr for sequential scan) Pipeline: INDEX ON expr TAG tagname TO file - ast.IndexCmd gains TagName field - Parser captures TAG name (was discarded) - gengo passes TagName to OrderCreateParams - indexer.go routes to cdx.CreateOrAddTag when TAG specified Verified: 3 tags (BYNAME/BYCITY/BYAGE), OrdSetFocus by name, SEEK, GoTop/GoBottom, close+reopen with SET INDEX TO 2. {||} empty code block parsing in function arguments Parser's parseArrayOrBlock() called parseExpr() unconditionally after closing |, failing when body was empty ({||}). Fix: check for RBRACE after closing | and emit NIL literal body. {=>} empty hash already worked. 3. Semicolon IF...ENDIF — already worked (removed from constraints) Tests: go test ./... 14 packages ALL PASS FiveSql2 43/43 100% compat_harbour 51/51 Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 9 +- compiler/ast/ast.go | 3 +- compiler/gengo/gengo.go | 4 +- compiler/parser/expr.go | 7 + compiler/parser/parser.go | 10 +- hbrdd/cdx/build.go | 511 ++++++++++++++++++++++++++++++++++++++ hbrdd/cdx/cdx.go | 6 + hbrdd/dbf/indexer.go | 62 ++++- 8 files changed, 590 insertions(+), 22 deletions(-) create mode 100644 hbrdd/cdx/build.go diff --git a/CLAUDE.md b/CLAUDE.md index 8863953..aa066c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,16 +65,15 @@ cd ~/tmp && rm -f *.dbf __cte_*.dbf && /tmp/test_sql ## 알려진 제약사항 -| 항목 | 상태 | 우회 방법 | -|------|------|----------| -| 세미콜론 IF...ENDIF | Five 파서 미지원 | 여러 줄로 분리 | -| `{||}` / `{=>}` 함수 인자 내 | 파서가 빈 블록/해시를 인자 위치에서 파싱 실패 | 변수에 담아서 전달 | -| CDX compound index 쓰기 | 읽기만 지원, 생성 불가 | NTX 사용 또는 Harbour로 CDX 생성 | +현재 알려진 제약사항 없음. 모든 이전 제약이 해결됨. ### 해결된 제약 (2026-04-11~13) | 항목 | 커밋 | |------|------| +| 세미콜론 IF...ENDIF | 이미 동작 확인 (2026-04-13) | +| `{||}` / `{=>}` 함수 인자 파싱 실패 | 빈 블록 body에 NIL 리터럴 emit | +| CDX compound index 쓰기 미지원 | CDX 빌더 구현 (비트팩 리프+compound root) | | STATIC inside FUNCTION → panic | `5bfdc47` — Go 패키지 변수로 emit | | FIELD->NAME 빈 값 반환 | `e95afad` — GetAliasField 반환 타입 수정 | | OrdSetFocus(n) 무동작 | `e95afad` — 숫자→문자열 변환 수정 | diff --git a/compiler/ast/ast.go b/compiler/ast/ast.go index 0367deb..fbd3b1a 100644 --- a/compiler/ast/ast.go +++ b/compiler/ast/ast.go @@ -855,12 +855,13 @@ func (s *DeleteCmd) Pos() token.Position { return s.DeletePos } func (s *DeleteCmd) End() token.Position { return s.DeletePos } func (s *DeleteCmd) stmtNode() {} -// IndexCmd represents INDEX ON expr TO file [FOR cond] [UNIQUE] [DESCENDING] +// IndexCmd represents INDEX ON expr [TAG tagname] TO file [FOR cond] [UNIQUE] [DESCENDING] type IndexCmd struct { IndexPos token.Position KeyExpr Expr File Expr ForCond Expr // nil if no FOR + TagName string // TAG name for CDX compound index (empty = NTX) Unique bool Descending bool } diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 4cac17a..ad727da 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -626,8 +626,8 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) { } // Set VM callback for UDF evaluation during index build g.writeln("dbf.KeyEvalFunc = func(expr string) hbrt.Value { return t.MacroEval(expr) }") - g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, Unique: %v, Descending: %v})", - forExpr, s.Unique, s.Descending)) + g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, TagName: %q, Unique: %v, Descending: %v})", + forExpr, s.TagName, s.Unique, s.Descending)) g.writeln("dbf.KeyEvalFunc = nil") g.indent-- g.writeln("}") diff --git a/compiler/parser/expr.go b/compiler/parser/expr.go index 4a218ac..bc6afbc 100644 --- a/compiler/parser/expr.go +++ b/compiler/parser/expr.go @@ -418,6 +418,13 @@ func (p *Parser) parseArrayOrBlock() ast.Expr { } p.expect(token.PIPE) // closing | + // Empty block body: {||} or {|x|} → body is NIL + if p.at(token.RBRACE) { + rbrace := p.advance().Pos + nilBody := &ast.LiteralExpr{ValuePos: rbrace, Kind: token.NIL_LIT, Value: "NIL"} + return &ast.BlockExpr{LBrace: lbrace, Params: params, Body: nilBody, RBrace: rbrace} + } + // Parse block body — may have comma-separated expressions // {|x| expr1, expr2} → comma = sequence, returns last value body := p.parseExpr() diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index c000de1..875cc98 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1711,18 +1711,20 @@ func (p *Parser) parseIndex() *ast.IndexCmd { p.expect(token.ON) keyExpr := p.parseExpr() - // INDEX ON expr TO file OR INDEX ON expr TAG tagname [TO file] + // INDEX ON expr [TAG tagname] TO file [FOR cond] [UNIQUE] [DESCENDING] var fileExpr ast.Expr + var tagName string if p.match(token.TO) { fileExpr = p.parseExpr() p.consumeFileExtension(fileExpr) } else if p.current.Kind == token.IDENT && p.currentUpper() == "TAG" { p.advance() // skip TAG - tagExpr := p.parseExpr() // tag name + tagName = p.expectMethodName().Literal // capture tag name if p.match(token.TO) { fileExpr = p.parseExpr() } else { - fileExpr = tagExpr // use tag name as file + // TAG without TO: use tag name as file name + fileExpr = &ast.IdentExpr{NamePos: p.current.Pos, Name: tagName} } } else { fileExpr = p.parseExpr() // fallback @@ -1747,7 +1749,7 @@ func (p *Parser) parseIndex() *ast.IndexCmd { return &ast.IndexCmd{ IndexPos: pos, KeyExpr: keyExpr, File: fileExpr, - ForCond: forCond, Unique: unique, Descending: descending, + ForCond: forCond, TagName: tagName, Unique: unique, Descending: descending, } } diff --git a/hbrdd/cdx/build.go b/hbrdd/cdx/build.go new file mode 100644 index 0000000..ec25e00 --- /dev/null +++ b/hbrdd/cdx/build.go @@ -0,0 +1,511 @@ +// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) +// All rights reserved. + +// CDX compound index creation. +// Builds Harbour-compatible CDX files with bit-packed leaf pages. + +package cdx + +import ( + "encoding/binary" + "io" + "math/bits" + "os" + "strings" + + "five/hbrdd/ntx" +) + +// cdxTagMeta holds metadata for a tag during file assembly. +type cdxTagMeta struct { + name string + headerOff int64 + rootPage uint32 + keyExpr string + forExpr string + keyLen int + unique bool + desc bool +} + +// CreateOrAddTag creates a CDX file with a new tag, or appends a tag to +// an existing CDX file. The existing file's bytes are preserved verbatim; +// only the compound root directory is rebuilt to include the new tag. +// +// Harbour: ordCreate() → hb_cdxIndexCreateTag() +func CreateOrAddTag(path string, tagName, keyExpr, forExpr string, + keyLen int, unique, descending bool, keys []ntx.KeyRecord) (*Index, error) { + + if !strings.HasSuffix(strings.ToLower(path), ".cdx") { + path += ".cdx" + } + tagName = strings.ToUpper(tagName) + if len(tagName) > MaxTagNameLen { + tagName = tagName[:MaxTagNameLen] + } + + // Read existing file contents (if any) and tag metadata + var existingData []byte + var existingTags []cdxTagMeta + + if fi, err := os.Stat(path); err == nil && fi.Size() > 0 { + existing, err := OpenIndex(path) + if err == nil { + for _, t := range existing.Tags() { + if strings.ToUpper(t.Name) == tagName { + continue // will be replaced + } + existingTags = append(existingTags, cdxTagMeta{ + name: t.Name, + headerOff: t.HeaderOffset(), + rootPage: t.RootPtr(), + keyExpr: t.KeyExpr(), + forExpr: t.ForExpr(), + keyLen: t.KeyLen(), + }) + } + existing.Close() + } + // Read entire old file + existingData, _ = os.ReadFile(path) + } + + // Create/truncate the file + f, err := os.Create(path) + if err != nil { + return nil, err + } + + var appendOff int64 // where to start writing new data + + if len(existingData) > 0 { + // Write back existing data verbatim (preserves all old tag B-trees) + f.Write(existingData) + appendOff = int64(len(existingData)) + // Align to HeaderLen boundary for the new tag header + if appendOff%int64(HeaderLen) != 0 { + appendOff = (appendOff/int64(HeaderLen) + 1) * int64(HeaderLen) + } + } else { + // New file: reserve space for compound root header + appendOff = int64(HeaderLen) + } + + // Write the new tag's header + B-tree + newTagHeaderOff := appendOff + appendOff += int64(HeaderLen) // reserve header space + + // Build B-tree pages for the new tag + var rootPageOff uint32 + if len(keys) == 0 { + pg := make([]byte, PageLen) + binary.LittleEndian.PutUint16(pg[0:2], NodeLeaf|NodeRoot) + f.WriteAt(pg, appendOff) + rootPageOff = uint32(appendOff) + appendOff += PageLen + } else { + root, next := buildCDXBTree(f, keys, keyLen, appendOff) + rootPageOff = uint32(root) + appendOff = next + } + + // Write the new tag header + writeTagHeader(f, newTagHeaderOff, rootPageOff, keyExpr, forExpr, + uint16(keyLen), unique, descending) + + newTag := cdxTagMeta{ + name: tagName, headerOff: newTagHeaderOff, rootPage: rootPageOff, + keyExpr: keyExpr, forExpr: forExpr, keyLen: keyLen, + unique: unique, desc: descending, + } + + // Collect all tags (existing + new) in offset order (= creation order) + allTags := append(existingTags, newTag) + + // Rebuild compound root directory page + compoundPageOff := appendOff + appendOff += PageLen + writeCompoundLeaf(f, compoundPageOff, allTags) + + // Write compound root header at offset 0 + writeCompoundHeader(f, uint32(compoundPageOff), len(allTags)) + + f.Close() + return OpenIndex(path) +} + +// --- Tag header writing --- + +func writeTagHeader(f *os.File, offset int64, rootPtr uint32, + keyExpr, forExpr string, keySize uint16, unique, desc bool) { + + buf := make([]byte, HeaderLen) + binary.LittleEndian.PutUint32(buf[0:4], rootPtr) + binary.LittleEndian.PutUint32(buf[8:12], 1) // counter + binary.LittleEndian.PutUint16(buf[12:14], keySize) + opt := byte(TypeCompact) + if unique { + opt |= TypeUnique + } + if forExpr != "" { + opt |= TypeForFilter + } + buf[14] = opt + buf[15] = 0x01 + binary.LittleEndian.PutUint16(buf[16:18], uint16(HeaderLen)) + binary.LittleEndian.PutUint16(buf[18:20], uint16(PageLen)) + if desc { + binary.LittleEndian.PutUint16(buf[504:506], 1) + } + copy(buf[512:], []byte(keyExpr)) + if forExpr != "" { + forOff := 512 + len(keyExpr) + 1 + if forOff+len(forExpr) < HeaderLen { + copy(buf[forOff:], []byte(forExpr)) + } + } + f.WriteAt(buf, offset) +} + +// --- Compound root --- + +func writeCompoundHeader(f *os.File, rootPagePtr uint32, nTags int) { + hdr := make([]byte, HeaderLen) + binary.LittleEndian.PutUint32(hdr[0:4], rootPagePtr) + binary.LittleEndian.PutUint32(hdr[8:12], 1) + binary.LittleEndian.PutUint16(hdr[12:14], MaxTagNameLen) + hdr[14] = TypeCompound | TypeStructure | TypeCompact + hdr[15] = 0x01 + binary.LittleEndian.PutUint16(hdr[16:18], uint16(HeaderLen)) + binary.LittleEndian.PutUint16(hdr[18:20], uint16(PageLen)) + f.WriteAt(hdr, 0) +} + +func writeCompoundLeaf(f *os.File, offset int64, tags []cdxTagMeta) { + leaf := make([]byte, PageLen) + nTags := len(tags) + compKeyLen := MaxTagNameLen + + maxOff := uint32(0) + for _, t := range tags { + if uint32(t.headerOff) > maxOff { + maxOff = uint32(t.headerOff) + } + } + + recBits := bitsNeeded(maxOff) + dupBits := bitsNeeded(uint32(compKeyLen)) + trlBits := bitsNeeded(uint32(compKeyLen)) + keyBytes := (recBits + dupBits + trlBits + 7) / 8 + + binary.LittleEndian.PutUint16(leaf[0:2], NodeLeaf|NodeRoot) + binary.LittleEndian.PutUint16(leaf[2:4], uint16(nTags)) + binary.LittleEndian.PutUint32(leaf[14:18], (1< 0 { + keyDataPos -= newBytes + copy(leaf[keyDataPos:], key[dup:dup+newBytes]) + } + + recNo := uint32(t.headerOff) + var val uint64 + val = uint64(trl) + val <<= uint(dupBits) + val |= uint64(dup) + val <<= uint(recBits) + val |= uint64(recNo) + + entryOff := ExtHeadSize + i*int(keyBytes) + for j := 0; j < int(keyBytes); j++ { + leaf[entryOff+j] = byte(val & 0xFF) + val >>= 8 + } + copy(prevKey, key) + } + + freeSpc := keyDataPos - (ExtHeadSize + nTags*int(keyBytes)) + if freeSpc < 0 { + freeSpc = 0 + } + binary.LittleEndian.PutUint16(leaf[12:14], uint16(freeSpc)) + f.WriteAt(leaf, offset) +} + +// --- B-tree builder --- + +func buildCDXBTree(f *os.File, keys []ntx.KeyRecord, keyLen int, + startOff int64) (rootOff int64, nextOff int64) { + + maxRecNo := uint32(0) + for _, k := range keys { + if k.RecNo > maxRecNo { + maxRecNo = k.RecNo + } + } + recBits := bitsNeeded(maxRecNo) + dupBits := bitsNeeded(uint32(keyLen)) + trlBits := bitsNeeded(uint32(keyLen)) + keyBytesPerEntry := (recBits + dupBits + trlBits + 7) / 8 + + maxKeysPerLeaf := (PageLen - ExtHeadSize) / (int(keyBytesPerEntry) + keyLen) + if maxKeysPerLeaf < 2 { + maxKeysPerLeaf = 2 + } + intEntrySize := keyLen + 8 + maxKeysPerInt := (PageLen - 12) / intEntrySize + if maxKeysPerInt < 2 { + maxKeysPerInt = 2 + } + + curOff := startOff + + type leafInfo struct { + pageOff int64 + lastKey []byte + lastRec uint32 + } + var leaves []leafInfo + + for i := 0; i < len(keys); { + end := i + maxKeysPerLeaf + if end > len(keys) { + end = len(keys) + } + chunk := keys[i:end] + pageOff := curOff + writeCDXLeafPage(f, pageOff, chunk, keyLen, recBits, dupBits, trlBits, keyBytesPerEntry) + curOff += PageLen + leaves = append(leaves, leafInfo{ + pageOff: pageOff, + lastKey: chunk[len(chunk)-1].Key, + lastRec: chunk[len(chunk)-1].RecNo, + }) + i = end + } + + // Link leaf pages + for i := range leaves { + pg := make([]byte, PageLen) + f.ReadAt(pg, leaves[i].pageOff) + if i > 0 { + binary.LittleEndian.PutUint32(pg[4:8], uint32(leaves[i-1].pageOff)) + } + if i < len(leaves)-1 { + binary.LittleEndian.PutUint32(pg[8:12], uint32(leaves[i+1].pageOff)) + } + f.WriteAt(pg, leaves[i].pageOff) + } + + if len(leaves) == 1 { + pg := make([]byte, 2) + f.ReadAt(pg, leaves[0].pageOff) + attr := binary.LittleEndian.Uint16(pg) + attr |= NodeRoot + binary.LittleEndian.PutUint16(pg, attr) + f.WriteAt(pg, leaves[0].pageOff) + return leaves[0].pageOff, curOff + } + + type childInfo struct { + pageOff int64 + sepKey []byte + sepRec uint32 + } + children := make([]childInfo, len(leaves)) + for i, l := range leaves { + children[i] = childInfo{pageOff: l.pageOff, sepKey: l.lastKey, sepRec: l.lastRec} + } + + for len(children) > 1 { + var nextLevel []childInfo + for i := 0; i < len(children); { + end := i + maxKeysPerInt + 1 + if end > len(children) { + end = len(children) + } + group := children[i:end] + nKeys := len(group) - 1 + + pg := make([]byte, PageLen) + attr := uint16(0) + if len(children) <= maxKeysPerInt+1 { + attr |= NodeRoot + } + binary.LittleEndian.PutUint16(pg[0:2], attr) + binary.LittleEndian.PutUint16(pg[2:4], uint16(nKeys)) + binary.LittleEndian.PutUint32(pg[4:8], uint32(group[0].pageOff)) + + off := 12 + for k := 0; k < nKeys; k++ { + key := group[k].sepKey + if len(key) < keyLen { + padded := make([]byte, keyLen) + copy(padded, key) + for j := len(key); j < keyLen; j++ { + padded[j] = ' ' + } + key = padded + } + copy(pg[off:off+keyLen], key[:keyLen]) + off += keyLen + binary.BigEndian.PutUint32(pg[off:off+4], group[k].sepRec) + off += 4 + binary.BigEndian.PutUint32(pg[off:off+4], uint32(group[k+1].pageOff)) + off += 4 + } + + pageOff := curOff + f.WriteAt(pg, pageOff) + curOff += PageLen + + ci := childInfo{pageOff: pageOff} + if end < len(children) { + ci.sepKey = group[nKeys].sepKey + ci.sepRec = group[nKeys].sepRec + } + nextLevel = append(nextLevel, ci) + i = end + } + children = nextLevel + } + + return children[0].pageOff, curOff +} + +// --- Leaf page writer --- + +func writeCDXLeafPage(f *os.File, offset int64, keys []ntx.KeyRecord, + keyLen, recBits, dupBits, trlBits, keyBytesPerEntry int) { + + pg := make([]byte, PageLen) + binary.LittleEndian.PutUint16(pg[0:2], NodeLeaf) + binary.LittleEndian.PutUint16(pg[2:4], uint16(len(keys))) + binary.LittleEndian.PutUint32(pg[14:18], (1< keyLen { + key = key[:keyLen] + } + + dup := commonPrefix(key, prevKey, keyLen) + trl := trailingSpaces(key, keyLen) + newBytes := keyLen - dup - trl + + if newBytes > 0 { + keyDataPos -= newBytes + copy(pg[keyDataPos:], key[dup:dup+newBytes]) + } + + var val uint64 + val = uint64(trl) + val <<= uint(dupBits) + val |= uint64(dup) + val <<= uint(recBits) + val |= uint64(kr.RecNo) + + entryOff := ExtHeadSize + i*keyBytesPerEntry + for j := 0; j < keyBytesPerEntry; j++ { + pg[entryOff+j] = byte(val & 0xFF) + val >>= 8 + } + copy(prevKey, key) + } + + freeSpc := keyDataPos - (ExtHeadSize + len(keys)*keyBytesPerEntry) + if freeSpc < 0 { + freeSpc = 0 + } + binary.LittleEndian.PutUint16(pg[12:14], uint16(freeSpc)) + f.WriteAt(pg, offset) +} + +// --- Helpers --- + +func bitsNeeded(maxVal uint32) int { + if maxVal == 0 { + return 1 + } + return bits.Len32(maxVal) +} + +func commonPrefix(a, b []byte, maxLen int) int { + n := maxLen + if len(a) < n { + n = len(a) + } + if len(b) < n { + n = len(b) + } + for i := 0; i < n; i++ { + if a[i] != b[i] { + return i + } + } + return n +} + +func trailingSpaces(key []byte, keyLen int) int { + n := keyLen + if len(key) < n { + n = len(key) + } + count := 0 + for i := n - 1; i >= 0; i-- { + if key[i] == ' ' { + count++ + } else { + break + } + } + return count +} + +func padTagName(name string) []byte { + b := make([]byte, MaxTagNameLen) + copy(b, []byte(strings.ToUpper(name))) + for i := len(name); i < MaxTagNameLen; i++ { + b[i] = ' ' + } + return b +} + +// Ensure io import is used (for potential future reads) +var _ = io.EOF diff --git a/hbrdd/cdx/cdx.go b/hbrdd/cdx/cdx.go index ea9dcbc..93c9f20 100644 --- a/hbrdd/cdx/cdx.go +++ b/hbrdd/cdx/cdx.go @@ -937,6 +937,12 @@ func (t *Tag) ForExpr() string { return t.header.ForExpr } // IsDescending returns true if the tag sorts in descending order. func (t *Tag) IsDescending() bool { return t.header.Descending } +// HeaderOffset returns the file offset of this tag's header block. +func (t *Tag) HeaderOffset() int64 { return t.headerOff } + +// RootPtr returns the root page offset for this tag's B-tree. +func (t *Tag) RootPtr() uint32 { return t.header.RootPtr } + // Close is a no-op for tags (the parent Index owns the file). func (t *Tag) Close() error { return nil } diff --git a/hbrdd/dbf/indexer.go b/hbrdd/dbf/indexer.go index d487cee..e7d7ce0 100644 --- a/hbrdd/dbf/indexer.go +++ b/hbrdd/dbf/indexer.go @@ -105,9 +105,14 @@ func (a *DBFArea) OrderCreate(params hbrdd.OrderCreateParams) error { return fmt.Errorf("index file path required") } - // Ensure .ntx extension + // Determine index format: CDX if TAG specified or .cdx extension, otherwise NTX + useCDX := params.TagName != "" || strings.HasSuffix(strings.ToLower(idxPath), ".cdx") if !strings.Contains(filepath.Base(idxPath), ".") { - idxPath += ".ntx" + if useCDX { + idxPath += ".cdx" + } else { + idxPath += ".ntx" + } } // Build key evaluator from expression @@ -232,15 +237,52 @@ func (a *DBFArea) OrderCreate(params hbrdd.OrderCreateParams) error { sort.Sort(keyRecordAsc(keys)) } - idx, err := ntx.CreateIndex(idxPath, keyExpr, keyLen, params.Unique, params.Descending, keys) - if err != nil { - return fmt.Errorf("create index failed: %w", err) + if useCDX { + // CDX compound index — append tag to existing file or create new + tagName := params.TagName + if tagName == "" { + tagName = keyExpr // default tag name = key expression + } + ci, err := cdx.CreateOrAddTag(idxPath, tagName, keyExpr, params.ForExpr, + keyLen, params.Unique, params.Descending, keys) + if err != nil { + return fmt.Errorf("create CDX index failed: %w", err) + } + // Register all tags from the CDX file + // If this is the first tag, add all; if adding to existing, re-register + // Remove old entries for this CDX file first + newIndexes := make([]IndexEngine, 0, len(a.idxState.indexes)+ci.TagCount()) + newNames := make([]string, 0, cap(newIndexes)) + newTags := make([]string, 0, cap(newIndexes)) + newKeyExprs := make([]string, 0, cap(newIndexes)) + for i, name := range a.idxState.names { + if name != idxPath { + newIndexes = append(newIndexes, a.idxState.indexes[i]) + newNames = append(newNames, a.idxState.names[i]) + newTags = append(newTags, a.idxState.tags[i]) + newKeyExprs = append(newKeyExprs, a.idxState.keyExprs[i]) + } + } + for _, tag := range ci.Tags() { + newIndexes = append(newIndexes, tag) + newNames = append(newNames, idxPath) + newTags = append(newTags, tag.Name) + newKeyExprs = append(newKeyExprs, tag.KeyExpr()) + } + a.idxState.indexes = newIndexes + a.idxState.names = newNames + a.idxState.tags = newTags + a.idxState.keyExprs = newKeyExprs + } else { + idx, err := ntx.CreateIndex(idxPath, keyExpr, keyLen, params.Unique, params.Descending, keys) + if err != nil { + return fmt.Errorf("create index failed: %w", err) + } + a.idxState.indexes = append(a.idxState.indexes, idx) + a.idxState.names = append(a.idxState.names, idxPath) + a.idxState.tags = append(a.idxState.tags, params.TagName) + a.idxState.keyExprs = append(a.idxState.keyExprs, keyExpr) } - - a.idxState.indexes = append(a.idxState.indexes, idx) - a.idxState.names = append(a.idxState.names, idxPath) - a.idxState.tags = append(a.idxState.tags, params.TagName) - a.idxState.keyExprs = append(a.idxState.keyExprs, keyExpr) a.idxState.current = len(a.idxState.indexes) - 1 return nil