// 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<= 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) }