Files
five/hbrdd/dbf/null.go
CharlesKWON f4ed42556b checkpoint: season-wide bug fix campaign + infra
Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2
SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved
as a single checkpoint before refactoring the parser to delegate xBase
command translation to the preprocessor.

Highlights:

FiveSql2 engine (_FiveSql2/src/)
- prefix-glob index attach -> explicit convention (<table>_pk.ntx,
  <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop
- DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt)
- COUNT(DISTINCT col) parsed + aggregated via hSeen hash
- UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent)
- DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT)
- Derived table FROM (SELECT...) + JOIN right-side derived
- Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect
- LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs)
- DATE literal round-trip validation (Feb 29 non-leap rejected)
- CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists
- AlterTable type dispatcher comma-wrapped (1-char type "A" no longer
  matches CHARACTER)

Compiler / runtime
- gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity)
- gengo split: emit_block.go, emit_stmt.go, folding.go extracted
- parser/stmtreg.go nudges
- hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*),
  windows debug stubs collapsed
- thread/vm/value/class/pcinterp tightening from panic traces

RDD layer (hbrdd/)
- dbf: null bitmap support (null.go + null_test.go), mmap split
  (mmap_posix.go / mmap_windows.go), byte-level numeric parse
- ntx/cdx: windows mmap parity
- workarea + mem RDD: cross-area state-bleed fixes

RTL (hbrtl/)
- errorlog rewrite with platform-specific FD (errorlog_fd_unix /
  errorlog_fd_other)
- sqlscan, sqlhelpers, indexrtl, datetime extensions

Gates green at checkpoint:
- go test ./...        : PASS
- FiveSql2 SQL:1999    : 43/43
- Harbour compat       : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:26:25 +09:00

127 lines
3.6 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// SQL NULL support via Harbour/VFP-style _NullFlags bitmap column.
//
// When a table is created with at least one nullable field the DBF engine
// appends a hidden system field named "_NullFlags" of type '0' (Harbour
// VFP convention). The field's length is ceil(nNullable/8) bytes; each
// nullable user field owns one bit. A set bit means "this field holds
// SQL NULL" — readers return NIL instead of the raw value, writers
// clear the bit on a non-NIL write.
//
// The _NullFlags descriptor carries FieldFlagSystem so the base area's
// FieldCount / GetFieldInfo never expose it, keeping existing SQL /
// SELECT * / PRG scan-column code paths blind to the hidden field.
//
// Reference: /mnt/d/harbour-core/src/rdd/dbf1.c — hb_dbfGetNullFlag,
// hb_dbfSetNullFlag. Harbour also uses this column to track VARCHAR
// length bits; Five only implements nullability for now.
package dbf
// buildNullIndex populates nullFieldsIdx (descriptor index of
// _NullFlags, -1 if none), nullBitOf (user-field descriptor index →
// bit number within _NullFlags), and publicFieldCount. Call after
// fieldDescs has been populated.
func (a *DBFArea) buildNullIndex() {
a.nullFieldsIdx = -1
a.nullBitOf = nil
for i := range a.fieldDescs {
if a.fieldDescs[i].GetName() == NullFlagsFieldName {
a.nullFieldsIdx = i
break
}
}
if a.nullFieldsIdx < 0 {
return
}
a.nullBitOf = make(map[int]int, 4)
bit := 0
for i := range a.fieldDescs {
if i == a.nullFieldsIdx {
continue
}
if a.fieldDescs[i].Flags&FieldFlagNullable != 0 {
a.nullBitOf[i] = bit
bit++
}
}
}
// isFieldNull reports whether the given descriptor index currently
// holds SQL NULL. Only meaningful for fields marked nullable.
func (a *DBFArea) isFieldNull(fieldIdx int) bool {
if a.nullFieldsIdx < 0 || a.nullBitOf == nil {
return false
}
bit, ok := a.nullBitOf[fieldIdx]
if !ok {
return false
}
off := a.offsets[a.nullFieldsIdx]
byteIdx := bit / 8
bitIdx := bit % 8
if int(off)+byteIdx >= len(a.recBuf) {
return false
}
return a.recBuf[int(off)+byteIdx]&(1<<uint(bitIdx)) != 0
}
// setFieldNull sets or clears the SQL NULL bit for the given
// descriptor index. Caller is responsible for having COW-ed recBuf
// (PutValue does this before calling).
func (a *DBFArea) setFieldNull(fieldIdx int, isNull bool) {
if a.nullFieldsIdx < 0 || a.nullBitOf == nil {
return
}
bit, ok := a.nullBitOf[fieldIdx]
if !ok {
return
}
off := a.offsets[a.nullFieldsIdx]
byteIdx := bit / 8
bitIdx := bit % 8
if int(off)+byteIdx >= len(a.recBuf) {
return
}
mask := byte(1) << uint(bitIdx)
if isNull {
a.recBuf[int(off)+byteIdx] |= mask
} else {
a.recBuf[int(off)+byteIdx] &^= mask
}
}
// countNullableFields returns the number of user fields (non-system)
// marked nullable — used at CREATE time to size the _NullFlags column.
func countNullableFields(fields []FieldDesc) int {
n := 0
for i := range fields {
if fields[i].Flags&FieldFlagSystem != 0 {
continue
}
if fields[i].Flags&FieldFlagNullable != 0 {
n++
}
}
return n
}
// appendNullFlagsField returns fields with an appended _NullFlags
// system field sized to hold one bit per nullable user field. If no
// fields are nullable the input is returned unchanged.
func appendNullFlagsField(fields []FieldDesc) []FieldDesc {
n := countNullableFields(fields)
if n == 0 {
return fields
}
nBytes := (n + 7) / 8
var fd FieldDesc
fd.SetName(NullFlagsFieldName)
fd.Type = '0' // Harbour VFP convention for _NullFlags
fd.Len = byte(nBytes)
fd.Dec = 0
fd.Flags = FieldFlagSystem | FieldFlagBinary
return append(fields, fd)
}