Skip to content

Add BIT support#117

Open
JelteF wants to merge 1 commit intoduckdb:mainfrom
JelteF:bit-support
Open

Add BIT support#117
JelteF wants to merge 1 commit intoduckdb:mainfrom
JelteF:bit-support

Conversation

@JelteF
Copy link

@JelteF JelteF commented Feb 9, 2026

This adds support for reading/writing the BIT type. Since there is no native type for representing bitstrings in Go, this adds a custom Go type that represents one. It uses the same internal storage as DuckDB so that conversions are cheap and require minimal copying.

P.S. Strictly speaking there is the BitString type in the encoding/asn1, but that's not meant for general purpose use. It's really only there so that X509 certificates can be decoded. It's also laid out very differently than DuckDB its BIT type (e.g. it uses 0's for padding instead of 1's).

Copy link
Member

@mlafeldt mlafeldt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Found some minor inconsistencies and missing tests. Otherwise looks great.

types.go Outdated
}

// NewBitFromString creates a Bit from a string of '0' and '1' characters.
func NewBitFromString(s string) (*Bit, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns *Bit while all other type constructors return values - should be Bit, error) for consistency

value.go Outdated
case TYPE_BIT:
bit := mapping.GetBit(v)
defer mapping.DestroyBit(&bit)
return Bit{data: mapping.BitMembers(&bit)}, nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file, TYPE_BIT is still marked as "Invalid or unsupported" in isPrimitiveType() and missing from inferPrimitiveType()

types.go Outdated
// The slice is consumed directly without copying, so the caller must not modify it afterwards.
func NewBitFromRaw(data []byte) (*Bit, error) {
if len(data) <= 1 {
return &Bit{}, nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicky inconsistency:

  • NewBitFromString("") → error ("empty bit string")
  • NewBitFromRaw(nil) → success, returns &Bit{}

require.Equal(t, expected, r)
}

func TestAppenderBit(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also test scanning non-null into var r *Bit?

This adds support for reading/writing the BIT type. Since there is no
native type for representing bitstrings in Go, this adds a custom Go
type that represents one. It uses the same internal storage as DuckDB so
that conversions are cheap and require minimal copying.

P.S. Strictly speaking there is the BitString type in the encoding/asn1,
but that's not meant for general purpose use. It's really only there so
that X509 certificates can be decoded. It's also laid out very
differently than DuckDB its BIT type (e.g. it uses 0's for padding
instead of 1's).
@JelteF
Copy link
Author

JelteF commented Feb 10, 2026

I think I addressed all of your comments.

I also removed NewBitFromRaw after trying these new APIs in my own project. Instead I now simply made the Data field public (I added a Validate() method so people can still check if it's stored in the correct format). The reason for doing so is that for my own usecase I wanted to access the raw bits and convert them to some other bitstring form, without going through the String representation. I could have added a getter method, but that would either have to create a copy of the bytes or return the actual slice. I wanted to do conversion to the other form with minimal amount of copies so that disqualified the first option. And having a getter return the actual slice, just seemed like a complicated way of effectively making the field public. So that's what I did.

Copy link
Member

@mlafeldt mlafeldt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes. I had another look and left a few more inline comments to address before merging.

// Validate checks that Data is a valid DuckDB bit encoding: the padding count
// (first byte) must be 0-7, and the padding bits in the first data byte must
// all be set to 1.
func (b Bit) Validate() error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is never called on any write path. Both setBit and bindBit pass Data (which is public) straight to DuckDB without validation.

// all be set to 1.
func (b Bit) Validate() error {
if len(b.Data) <= 1 {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to reject single-byte with nonzero padding

if len(b.Data) == 1 && b.Data[0] != 0 {
    return fmt.Errorf("invalid padding count %d with no data bytes", b.Data[0])
}

// NewBitFromString creates a Bit from a string of '0' and '1' characters.
func NewBitFromString(s string) (Bit, error) {
if len(s) == 0 {
return Bit{}, nil
Copy link
Member

@mlafeldt mlafeldt Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will return Bit{Data: nil}. Passing it to setBit or bindBit sends a nil slice into C code.

I suggest to either return an error for empty strings or return Bit{Data: []byte{0}} as the canonical empty encoding.

func (conn *Conn) CheckNamedValue(nv *driver.NamedValue) error {
switch nv.Value.(type) {
case *big.Int, Interval, []any, []bool, []int8, []int16, []int32, []int64, []int, []uint8, []uint16,
case *big.Int, Interval, Bit, []any, []bool, []int8, []int16, []int32, []int64, []int, []uint8, []uint16,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also handle *Bit

}
return mapping.BindInterval(*s.preparedStmt, mapping.IdxT(n+1), i), nil
case Bit:
return s.bindBit(&v, n)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also handle *Bit

// Complex type.
return false
case TYPE_INVALID, TYPE_BIT, TYPE_ANY:
case TYPE_INVALID, TYPE_ANY:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In value.go, Bit is still not handled in inferPrimitiveType and createPrimitiveValue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants