// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Index and database introspection RTL functions. // Harbour: INDEXORD, INDEXKEY, ORDSETFOCUS, ORDCOUNT, ORDNAME, ORDKEY, // ORDFOR, ORDSCOPE, DBORDERINFO, DBINFO, DBCREATE, RDDSETDEFAULT package hbrtl import ( "five/hbrt" "five/hbrdd" "five/hbrdd/dbf" "fmt" ) // INDEXORD() → nCurrentOrder (1-based, 0 = natural) func IndexOrd(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { if da, ok := area.(*dbf.DBFArea); ok { t.RetInt(int64(da.CurrentOrder())) return } } } t.RetInt(0) } // INDEXKEY([nOrder]) → cKeyExpression func IndexKey(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { if da, ok := area.(*dbf.DBFArea); ok { n := da.CurrentOrder() if nParams >= 1 && !t.Local(1).IsNil() { n = t.Local(1).AsInt() } t.RetString(da.OrderKeyExpr(n)) return } } } t.RetString("") } // ORDSETFOCUS([nOrder|cTag [, cBagName]]) → nOldOrder func OrdSetFocus(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetInt(0) return } area := wam.Current() if area == nil { t.RetInt(0) return } da, isDa := area.(*dbf.DBFArea) oldOrd := 0 if isDa { oldOrd = da.CurrentOrder() } if nParams >= 1 && !t.Local(1).IsNil() { if idx, ok := area.(hbrdd.Indexer); ok { v := t.Local(1) if v.IsNumeric() { // SET ORDER TO n — convert number to digit string for OrderListFocus idx.OrderListFocus(fmt.Sprintf("%d", v.AsNumInt())) } else { idx.OrderListFocus(v.AsString()) } } } t.RetInt(int64(oldOrd)) } // ORDCOUNT([cBagName]) → nOrders func OrdCount(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { if da, ok := area.(*dbf.DBFArea); ok { t.RetInt(int64(da.IndexCount())) return } } } t.RetInt(0) } // ORDLISTREBUILD — REINDEX equivalent. Rebuilds every attached index // from current DBF data. Called at the tail of SQL DML (INSERT / // UPDATE / DELETE) because `DBFArea.Append` / `PutValue` / `Delete` // don't yet have per-key ordKeyAdd / ordKeyDel hooks — the full // rebuild is the sledge-hammer that keeps the NTX on disk in sync // with the .dbf. No-op when no index is attached. func OrdListRebuild(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } if idx, ok := area.(hbrdd.Indexer); ok { _ = idx.OrderListRebuild() } t.RetNil() } // ORDNAME([nOrder [, cBagName]]) → cTagName func OrdName(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { if da, ok := area.(*dbf.DBFArea); ok { n := da.CurrentOrder() if nParams >= 1 && !t.Local(1).IsNil() { n = t.Local(1).AsInt() } t.RetString(da.OrderName(n)) return } } } t.RetString("") } // ORDKEY([nOrder [, cBagName]]) → cKeyExpression func OrdKey(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam != nil { if area := wam.Current(); area != nil { if da, ok := area.(*dbf.DBFArea); ok { n := da.CurrentOrder() if nParams >= 1 && !t.Local(1).IsNil() { n = t.Local(1).AsInt() } t.RetString(da.OrderKeyExpr(n)) return } } } t.RetString("") } // ORDFOR([nOrder [, cBagName]]) → cForExpression func OrdFor(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() // TODO: return FOR expression from index t.RetString("") } // ORDSCOPE(nScope [, xValue]) → xOldValue // nScope: 0 = TOPSCOPE, 1 = BOTTOMSCOPE // If xValue omitted, returns current scope. If xValue given, sets scope and returns old. // Harbour: TOPSCOPE = 0, BOTTOMSCOPE = 1 func OrdScope(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } da, ok := area.(*dbf.DBFArea) if !ok { t.RetNil() return } nScope := 0 if nParams >= 1 { nScope = t.Local(1).AsInt() } // Get old scope value var oldScope []byte if nScope == 0 { oldScope = da.GetScopeTop() } else { oldScope = da.GetScopeBottom() } if oldScope != nil { t.PushString(string(oldScope)) } else { t.PushNil() } // Set new scope if value provided if nParams >= 2 { val := t.Local(2) if val.IsNil() { if nScope == 0 { da.ClearScopeTop() } else { da.ClearScopeBottom() } } else { if nScope == 0 { da.SetScopeTop(val) } else { da.SetScopeBottom(val) } } } t.RetValue() } // DBI_* constants. Mirror include/dbinfo.ch. Only the ones we actually // answer are listed — unknown codes return NIL. const ( dbiIsDBF = 1 dbiCanPutRec = 2 dbiGetHeaderSize = 3 dbiLastUpdate = 4 dbiGetRecSize = 7 dbiTableExt = 9 dbiFullPath = 10 dbiMemoExt = 11 dbiDBVersion = 12 dbiRDDVersion = 13 dbiShared = 42 dbiIsReadOnly = 43 dbiPositioned = 45 dbiLockCount = 49 dbiBOF = 51 dbiEOF = 52 dbiFound = 54 dbiFCount = 55 dbiAlias = 56 ) // DBINFO(nInfoType [, xNewSetting]) → xInfo // // Queries workarea metadata. Only the setters that change observable // state are implemented; unknown info codes return NIL (Harbour's // forgiving behavior). xNewSetting is accepted but only honored for // fields where it makes sense. func DbInfo(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } if nParams < 1 { t.RetNil() return } nInfo := int(t.Local(1).AsNumInt()) // DBF-specific queries if da, ok := area.(*dbf.DBFArea); ok { switch nInfo { case dbiIsDBF: t.RetBool(true) return case dbiCanPutRec: t.RetBool(!da.IsReadOnly()) return case dbiFullPath: t.RetString(da.FullPath()) return case dbiTableExt: t.RetString(".dbf") return case dbiMemoExt: if da.MemoFile() != nil { t.RetString(".fpt") } else { t.RetString("") } return case dbiShared: t.RetBool(da.IsShared()) return case dbiIsReadOnly: t.RetBool(da.IsReadOnly()) return case dbiGetRecSize: nCount, _ := da.RecCount() _ = nCount // Header + records length — approximation from FieldInfo total := 0 for i := 0; i < da.FieldCount(); i++ { total += da.GetFieldInfo(i).Len } t.RetInt(int64(total + 1)) // +1 for delete flag return case dbiDBVersion: t.RetString("Five DBF 1.0") return case dbiRDDVersion: t.RetString("Five 1.0") return } } // Generic (any Area) queries switch nInfo { case dbiBOF: t.RetBool(area.BOF()) return case dbiEOF: t.RetBool(area.EOF()) return case dbiFound: t.RetBool(area.Found()) return case dbiFCount: t.RetInt(int64(area.FieldCount())) return case dbiAlias: t.RetString(area.Alias()) return case dbiPositioned: t.RetBool(!area.BOF() && !area.EOF()) return } t.RetNil() } // DBOI_* constants. Mirror include/dbinfo.ch. const ( dboiCondition = 1 dboiExpression = 2 dboiPosition = 3 dboiName = 4 dboiNumber = 5 dboiBagName = 6 dboiBagExt = 7 dboiIndexName = 8 dboiOrderCount = 9 dboiIsCond = 11 dboiIsDesc = 12 dboiUnique = 13 dboiKeyType = 14 dboiKeySize = 15 dboiKeyCount = 22 dboiKeyCountRaw = 34 ) // DBORDERINFO(nInfoType [, cBagName [, nOrder [, xNewSetting]]]) → xInfo // // Queries metadata about an active order (index). The order is identified // by nOrder (1-based) or defaults to the current focus. func DbOrderInfo(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } da, ok := area.(*dbf.DBFArea) if !ok { t.RetNil() return } if nParams < 1 { t.RetNil() return } nInfo := int(t.Local(1).AsNumInt()) // Resolve which order we're asking about. ord := da.CurrentOrder() if nParams >= 3 && !t.Local(3).IsNil() { ord = int(t.Local(3).AsNumInt()) } switch nInfo { case dboiExpression: t.RetString(da.OrderKeyExpr(ord)) return case dboiName: t.RetString(da.OrderName(ord)) return case dboiNumber, dboiPosition: t.RetInt(int64(ord)) return case dboiOrderCount: t.RetInt(int64(da.IndexCount())) return case dboiKeyCount, dboiKeyCountRaw: n, _ := da.RecCount() t.RetInt(int64(n)) return case dboiKeySize: // Byte length of the stored keys for this order. TSqlIndex:BuildKey // uses this to right-size numeric scope keys — otherwise a hard-coded // Str(xValue, 10) produces bytes that don't align with the 8-byte // index keys for N(8,0) columns, and ordScope silently fails to // constrain the scan. t.RetInt(int64(da.OrderKeyLen(ord))) return } t.RetNil() } // ORDINFO(nInfoType [, cOrder]) → xInfo func OrdInfo(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() t.RetNil() } // RDDSETDEFAULT([cDriver]) → cOldDriver func RddSetDefault(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() t.RetString("DBFNTX") } // FIELDTYPE(n) → cType — one-letter type ("C"/"N"/"L"/"D"/"M"/...) // Harbour: field descriptor type byte from current workarea. func FieldType(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetString("") return } area := wam.Current() if area == nil { t.RetString("") return } n := t.Local(1).AsInt() - 1 if n < 0 || n >= area.FieldCount() { t.RetString("") return } fi := area.GetFieldInfo(n) t.RetString(string(fi.Type)) } // FIELDLEN(n) → nLen — field length in bytes. func FieldLen(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetInt(0) return } area := wam.Current() if area == nil { t.RetInt(0) return } n := t.Local(1).AsInt() - 1 if n < 0 || n >= area.FieldCount() { t.RetInt(0) return } fi := area.GetFieldInfo(n) t.RetInt(int64(fi.Len)) } // FIELDDEC(n) → nDecimals — field decimal places. func FieldDec(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetInt(0) return } area := wam.Current() if area == nil { t.RetInt(0) return } n := t.Local(1).AsInt() - 1 if n < 0 || n >= area.FieldCount() { t.RetInt(0) return } fi := area.GetFieldInfo(n) t.RetInt(int64(fi.Dec)) } // ORDCREATE(cBagName, cTagName, cKeyExpr [, bKeyExpr] [, lUnique]) // Creates a new index (CDX tag or NTX file). Uses MacroEval slow path // for the key expression since Callers pass a string literal. func OrdCreate(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } idx, ok := area.(hbrdd.Indexer) if !ok { t.RetNil() return } cBag := "" if nParams >= 1 && !t.Local(1).IsNil() { cBag = t.Local(1).AsString() } cTag := "" if nParams >= 2 && !t.Local(2).IsNil() { cTag = t.Local(2).AsString() } cExpr := "" if nParams >= 3 && !t.Local(3).IsNil() { cExpr = t.Local(3).AsString() } lUnique := false if nParams >= 5 && !t.Local(5).IsNil() { lUnique = t.Local(5).AsBool() } _ = idx.OrderCreate(hbrdd.OrderCreateParams{ TagName: cTag, KeyExpr: cExpr, FilePath: cBag, Unique: lUnique, }) t.RetNil() } // DBCREATEINDEX(cFile, cKeyExpr [, bKeyExpr] [, lUnique]) // Legacy (Clipper) single-tag NTX index creation. Tag name defaults // to the bare filename. func DbCreateIndex(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } idx, ok := area.(hbrdd.Indexer) if !ok { t.RetNil() return } cFile := "" if nParams >= 1 && !t.Local(1).IsNil() { cFile = t.Local(1).AsString() } cExpr := "" if nParams >= 2 && !t.Local(2).IsNil() { cExpr = t.Local(2).AsString() } lUnique := false if nParams >= 4 && !t.Local(4).IsNil() { lUnique = t.Local(4).AsBool() } _ = idx.OrderCreate(hbrdd.OrderCreateParams{ KeyExpr: cExpr, FilePath: cFile, Unique: lUnique, }) t.RetNil() } // DBCLEARINDEX() — close all open index bags on current workarea. func DbClearIndex(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() wam := getWA(t) if wam == nil { t.RetNil() return } area := wam.Current() if area == nil { t.RetNil() return } if idx, ok := area.(hbrdd.Indexer); ok { _ = idx.OrderListClear() } t.RetNil() } // DBCREATE(cFile, aStruct [, cDriver]) → NIL func DbCreate(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() cFile := t.Local(1).AsString() aStruct := t.Local(2) cDriver := "DBFNTX" if nParams >= 3 && !t.Local(3).IsNil() { cDriver = t.Local(3).AsString() } if !aStruct.IsArray() { t.RetNil() return } arr := aStruct.AsArray() fields := make([]hbrdd.FieldInfo, len(arr.Items)) for i, item := range arr.Items { row := item.AsArray() if row == nil || len(row.Items) < 4 { continue } fields[i] = hbrdd.FieldInfo{ Name: row.Items[0].AsString(), Type: row.Items[1].AsString()[0], Len: row.Items[2].AsInt(), Dec: row.Items[3].AsInt(), } // Optional 5th element: field flag byte (e.g. FieldFlagNullable // = 0x02). Pre-nullable callers pass 4-element rows and leave // Flags at zero, so the hidden _NullFlags column is only added // when a caller explicitly opts a column in. if len(row.Items) >= 5 && row.Items[4].IsNumeric() { fields[i].Flags = byte(row.Items[4].AsInt()) } } drv, err := hbrdd.GetDriver(cDriver) if err != nil { t.RetNil() return } drv.Create(hbrdd.CreateParams{ Path: cFile, Fields: fields, }) t.RetNil() }