Skip to content

Commit c42208f

Browse files
authored
feat: support decimal (#58)
1 parent be587ba commit c42208f

17 files changed

+1369
-162
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,36 @@ will now also serialize ``float64`` (double-precision) columns as binary.
188188
You might see a performance uplift if this is a dominant data type in your
189189
ingestion workload.
190190

191+
## Decimal columns
192+
193+
QuestDB server version 9.2.0 and newer supports decimal columns with arbitrary precision and scale.
194+
The Go client converts supported decimal values to QuestDB's text/binary wire format automatically:
195+
196+
- `DecimalColumn`: `questdb.Decimal`, including helpers like `questdb.NewDecimalFromInt64` and `questdb.NewDecimal`.
197+
- `DecimalColumnShopspring`: `github.com/shopspring/decimal.Decimal` values or pointers.
198+
- `DecimalColumnFromString`: `string` literals representing decimal values (validated at runtime).
199+
200+
```go
201+
price := qdb.NewDecimalFromInt64(12345, 2) // 123.45 with scale 2
202+
commission := qdb.NewDecimal(big.NewInt(-750), 4) // -0.0750 with scale 4
203+
204+
err = sender.
205+
Table("trades").
206+
Symbol("symbol", "ETH-USD").
207+
DecimalColumn("price", price).
208+
DecimalColumn("commission", commission).
209+
AtNow(ctx)
210+
```
211+
212+
To emit textual decimals, pass a validated string literal:
213+
214+
```go
215+
err = sender.
216+
Table("quotes").
217+
DecimalColumnFromString("mid", "1.23456").
218+
AtNow(ctx)
219+
```
220+
191221
## Pooled Line Senders
192222

193223
**Warning: Experimental feature designed for use with HTTP senders ONLY**

buffer.go

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ type arrayElemType byte
8686

8787
const (
8888
arrayElemDouble arrayElemType = 10
89-
arrayElemNull arrayElemType = 33
9089
)
9190

9291
// buffer is a wrapper on top of bytes.Buffer. It extends the
@@ -573,6 +572,77 @@ func (b *buffer) Float64Column(name string, val float64) *buffer {
573572
return b
574573
}
575574

575+
func (b *buffer) DecimalColumn(name string, val Decimal) *buffer {
576+
if val.isNull() {
577+
// Don't write null decimals
578+
return b
579+
}
580+
if !b.prepareForField() {
581+
return b
582+
}
583+
return b.decimalColumn(name, val)
584+
}
585+
586+
func (b *buffer) decimalColumn(name string, val Decimal) *buffer {
587+
if err := val.ensureValidScale(); err != nil {
588+
b.lastErr = err
589+
return b
590+
}
591+
b.lastErr = b.writeColumnName(name)
592+
if b.lastErr != nil {
593+
return b
594+
}
595+
b.WriteByte('=')
596+
b.WriteByte('=')
597+
b.WriteByte(decimalBinaryTypeCode)
598+
b.WriteByte((uint8)(val.scale))
599+
b.WriteByte(32 - val.offset)
600+
b.Write(val.unscaled[val.offset:])
601+
b.hasFields = true
602+
return b
603+
}
604+
605+
func (b *buffer) DecimalColumnFromString(name string, val string) *buffer {
606+
if !b.prepareForField() {
607+
return b
608+
}
609+
if err := validateDecimalText(val); err != nil {
610+
b.lastErr = err
611+
return b
612+
}
613+
b.lastErr = b.writeColumnName(name)
614+
if b.lastErr != nil {
615+
return b
616+
}
617+
b.WriteByte('=')
618+
b.WriteString(val)
619+
b.WriteByte('d')
620+
b.hasFields = true
621+
return b
622+
}
623+
624+
func (b *buffer) DecimalColumnShopspring(name string, val ShopspringDecimal) *buffer {
625+
if val == nil {
626+
return b
627+
}
628+
if b.lastErr != nil {
629+
return b
630+
}
631+
dec, err := convertShopspringDecimal(val)
632+
if err != nil {
633+
b.lastErr = err
634+
return b
635+
}
636+
if dec.isNull() {
637+
// Don't write null decimals
638+
return b
639+
}
640+
if !b.prepareForField() {
641+
return b
642+
}
643+
return b.decimalColumn(name, dec)
644+
}
645+
576646
func (b *buffer) Float64ColumnBinary(name string, val float64) *buffer {
577647
if !b.prepareForField() {
578648
return b
@@ -597,17 +667,17 @@ func (b *buffer) Float64ColumnBinary(name string, val float64) *buffer {
597667
}
598668

599669
func (b *buffer) Float64Array1DColumn(name string, values []float64) *buffer {
670+
if values == nil {
671+
// Don't write null arrays
672+
return b
673+
}
600674
if !b.prepareForField() {
601675
return b
602676
}
603677
b.lastErr = b.writeColumnName(name)
604678
if b.lastErr != nil {
605679
return b
606680
}
607-
if values == nil {
608-
b.writeNullArray()
609-
return b
610-
}
611681

612682
dim1 := len(values)
613683
if dim1 > MaxArrayElements {
@@ -629,6 +699,10 @@ func (b *buffer) Float64Array1DColumn(name string, values []float64) *buffer {
629699
}
630700

631701
func (b *buffer) Float64Array2DColumn(name string, values [][]float64) *buffer {
702+
if values == nil {
703+
// Don't write null arrays
704+
return b
705+
}
632706
if !b.prepareForField() {
633707
return b
634708
}
@@ -637,11 +711,6 @@ func (b *buffer) Float64Array2DColumn(name string, values [][]float64) *buffer {
637711
return b
638712
}
639713

640-
if values == nil {
641-
b.writeNullArray()
642-
return b
643-
}
644-
645714
// Validate array shape
646715
dim1 := len(values)
647716
var dim2 int
@@ -678,6 +747,10 @@ func (b *buffer) Float64Array2DColumn(name string, values [][]float64) *buffer {
678747
}
679748

680749
func (b *buffer) Float64Array3DColumn(name string, values [][][]float64) *buffer {
750+
if values == nil {
751+
// Don't write null arrays
752+
return b
753+
}
681754
if !b.prepareForField() {
682755
return b
683756
}
@@ -686,11 +759,6 @@ func (b *buffer) Float64Array3DColumn(name string, values [][][]float64) *buffer
686759
return b
687760
}
688761

689-
if values == nil {
690-
b.writeNullArray()
691-
return b
692-
}
693-
694762
// Validate array shape
695763
dim1 := len(values)
696764
var dim2, dim3 int
@@ -740,6 +808,10 @@ func (b *buffer) Float64Array3DColumn(name string, values [][][]float64) *buffer
740808
}
741809

742810
func (b *buffer) Float64ArrayNDColumn(name string, value *NdArray[float64]) *buffer {
811+
if value == nil {
812+
// Don't write null arrays
813+
return b
814+
}
743815
if !b.prepareForField() {
744816
return b
745817
}
@@ -748,11 +820,6 @@ func (b *buffer) Float64ArrayNDColumn(name string, value *NdArray[float64]) *buf
748820
return b
749821
}
750822

751-
if value == nil {
752-
b.writeNullArray()
753-
return b
754-
}
755-
756823
shape := value.Shape()
757824
numDims := value.NDims()
758825
// Write nDims
@@ -831,7 +898,7 @@ func (b *buffer) At(ts time.Time, sendTs bool) error {
831898
b.DiscardPendingMsg()
832899
return fmt.Errorf("table name was not provided: %w", errInvalidMsg)
833900
}
834-
if !b.hasTags && !b.hasFields {
901+
if !b.hasTags && !b.hasFields && !sendTs {
835902
b.DiscardPendingMsg()
836903
return fmt.Errorf("no symbols or columns were provided: %w", errInvalidMsg)
837904
}
@@ -855,11 +922,3 @@ func (b *buffer) writeFloat64ArrayHeader(dims byte) {
855922
b.WriteByte(byte(arrayElemDouble))
856923
b.WriteByte(dims)
857924
}
858-
859-
func (b *buffer) writeNullArray() {
860-
b.WriteByte('=')
861-
b.WriteByte('=')
862-
b.WriteByte(byte(arrayCode))
863-
b.WriteByte(byte(arrayElemNull))
864-
b.hasFields = true
865-
}

0 commit comments

Comments
 (0)