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>
127 lines
3.6 KiB
Go
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)
|
|
}
|