feat: CDX compound index write + {||} parsing + zero known constraints
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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` — 숫자→문자열 변환 수정 |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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("}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
511
hbrdd/cdx/build.go
Normal file
511
hbrdd/cdx/build.go
Normal file
@@ -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<<uint(recBits))-1)
|
||||
leaf[18] = byte((1 << uint(dupBits)) - 1)
|
||||
leaf[19] = byte((1 << uint(trlBits)) - 1)
|
||||
leaf[20] = byte(recBits)
|
||||
leaf[21] = byte(dupBits)
|
||||
leaf[22] = byte(trlBits)
|
||||
leaf[23] = byte(keyBytes)
|
||||
|
||||
prevKey := make([]byte, compKeyLen)
|
||||
for j := range prevKey {
|
||||
prevKey[j] = ' '
|
||||
}
|
||||
keyDataPos := PageLen
|
||||
|
||||
for i, t := range tags {
|
||||
key := padTagName(t.name)
|
||||
dup := commonPrefix(key, prevKey, compKeyLen)
|
||||
trl := trailingSpaces(key, compKeyLen)
|
||||
newBytes := compKeyLen - dup - trl
|
||||
|
||||
if newBytes > 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<<uint(recBits))-1)
|
||||
pg[18] = byte((1 << uint(dupBits)) - 1)
|
||||
pg[19] = byte((1 << uint(trlBits)) - 1)
|
||||
pg[20] = byte(recBits)
|
||||
pg[21] = byte(dupBits)
|
||||
pg[22] = byte(trlBits)
|
||||
pg[23] = byte(keyBytesPerEntry)
|
||||
|
||||
prevKey := make([]byte, keyLen)
|
||||
for j := range prevKey {
|
||||
prevKey[j] = ' '
|
||||
}
|
||||
keyDataPos := PageLen
|
||||
|
||||
for i, kr := range keys {
|
||||
key := kr.Key
|
||||
if len(key) < keyLen {
|
||||
padded := make([]byte, keyLen)
|
||||
copy(padded, key)
|
||||
for j := len(key); j < keyLen; j++ {
|
||||
padded[j] = ' '
|
||||
}
|
||||
key = padded
|
||||
} else if len(key) > 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
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user