golib/database/csvx/bool.go
AJ ONeal 59fae2e7ae
fix(csvx): read human null words as absent, unrecognized values as false
BoolishTimestamp defaulted unrecognized non-empty values to true: any
word it does not understand (a misspelling, "disabled", "banana")
behaved as on. Unrecognized values are now valid+false with the error
retained so callers can apply their own policy, and "disabled"/"disable"
are no longer true-like.

Both bool types now read the human "not supplied" set through one shared
isBoolNull sentinel: empty/whitespace-only, the exact PG dump sentinel
\N (case-sensitive), and the case-insensitive words null, \null, none,
unknown. Bool previously reached absent only by ParseBool failing; it
now matches BoolishTimestamp explicitly. Free-text types must not reuse
this set: most of these words are legitimate values there.
2026-10-23 23:00:57 -06:00

106 lines
1.7 KiB
Go

package csvx
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"strings"
)
// Bool is a nullable boolean that implements CSV, JSON, and driver interfaces.
type Bool struct {
Bool bool
Valid bool
}
func (b Bool) IsNil() bool { return !b.Valid }
func (b Bool) Value() (driver.Value, error) {
if !b.Valid {
return nil, nil
}
return b.Bool, nil
}
func (b *Bool) Scan(value any) error {
if value == nil {
b.Valid = false
b.Bool = false
return nil
}
switch v := value.(type) {
case bool:
b.Bool = v
b.Valid = true
case string:
if isBoolNull(v) {
b.Valid = false
b.Bool = false
return nil
}
parsed, err := strconv.ParseBool(strings.TrimSpace(v))
if err != nil {
b.Valid = false
b.Bool = false
return nil
}
b.Bool = parsed
b.Valid = true
case []byte:
return b.Scan(string(v))
case Bool:
*b = v
default:
return fmt.Errorf("csvx.Bool: cannot scan %T", value)
}
return nil
}
func (b Bool) MarshalCSV() ([]byte, error) {
if !b.Valid {
return nil, nil
}
return []byte(strconv.FormatBool(b.Bool)), nil
}
func (b *Bool) UnmarshalCSV(data []byte) error {
s := strings.TrimSpace(string(data))
if isBoolNull(s) {
b.Valid = false
b.Bool = false
return nil
}
parsed, err := strconv.ParseBool(s)
if err != nil {
b.Valid = false
b.Bool = false
return nil
}
b.Bool = parsed
b.Valid = true
return nil
}
func (b Bool) MarshalJSON() ([]byte, error) {
if !b.Valid {
return []byte("null"), nil
}
return json.Marshal(b.Bool)
}
func (b *Bool) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
b.Valid = false
b.Bool = false
return nil
}
var v bool
if err := json.Unmarshal(data, &v); err != nil {
return err
}
b.Bool = v
b.Valid = true
return nil
}