Merge pull request #1558 from molecula/timestamp

CORE-372: Add timestamp field type support
This commit is contained in:
Ben Johnson 2021-04-06 11:28:10 -06:00 committed by GitHub
commit 7e369aeac4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1731 additions and 838 deletions

View file

@ -444,6 +444,56 @@ func TestAPI_ImportValue(t *testing.T) {
}
})
t.Run("ValTimestampField", func(t *testing.T) {
t.Skip("TODO(benbjohnson): timestamp")
ctx := context.Background()
index := "valts"
field := "fts"
_, err := m1.API.CreateIndex(ctx, index, pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m1.API.CreateField(ctx, index, field, pilosa.OptFieldTypeTimestamp(pilosa.MinTimestamp, pilosa.MaxTimestamp, pilosa.TimeUnitSeconds))
if err != nil {
t.Fatalf("creating field: %v", err)
}
// Generate some records.
t0 := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
values := []time.Time{}
colIDs := []uint64{}
for i := 0; i < 10; i++ {
values = append(values, t0.AddDate(0, 1, 0))
colIDs = append(colIDs, uint64(i))
}
// Import data with keys to node1 and verify that it gets translated and
// forwarded to the owner of shard 0 (node0; because of offsetModHasher)
req := &pilosa.ImportValueRequest{
Index: index,
Field: field,
ColumnIDs: colIDs,
TimestampValues: values,
}
qcx := m1.API.Txf().NewQcx()
if err := m1.API.ImportValue(ctx, qcx, req); err != nil {
t.Fatal(err)
}
PanicOn(qcx.Finish())
query := fmt.Sprintf("Row(%s>6)", field)
// Query node0.
if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: query}); err != nil {
t.Fatal(err)
} else if ids := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(ids, colIDs[6:]) {
t.Fatalf("unexpected column keys: observerd %+v; expected '%+v'", ids, colIDs[6:])
}
})
t.Run("ValStringField", func(t *testing.T) {
ctx := context.Background()
index := "valstr"

View file

@ -233,7 +233,7 @@ func (d *diagnosticsCollector) EnrichWithSchemaProperties() {
numIndexes++
for _, field := range index.Fields() {
numFields++
if field.Type() == FieldTypeInt || field.Type() == FieldTypeDecimal {
if field.Type() == FieldTypeInt || field.Type() == FieldTypeDecimal || field.Type() == FieldTypeTimestamp {
bsiFieldCount++
}
if field.TimeQuantum() != "" {

View file

@ -1029,6 +1029,8 @@ func (e *executor) executeFieldValueCallShard(ctx context.Context, qcx *Qcx, fie
Scale: field.Options().Scale}
other.FloatVal = 0
other.Val = 0
} else if field.Type() == FieldTypeTimestamp {
other.TimestampVal = time.Unix(0, value*int64(TimeUnitNano(field.Options().TimeUnit)))
}
return other, nil
@ -2620,8 +2622,8 @@ func (e *executor) executeTopNShard(ctx context.Context, qcx *Qcx, index string,
n, _, err := c.UintArg("n")
if err != nil {
return nil, fmt.Errorf("executeTopNShard: %v", err)
} else if f := e.Holder.Field(index, fieldName); f != nil && (f.Type() == FieldTypeInt || f.Type() == FieldTypeDecimal) {
return nil, fmt.Errorf("cannot compute TopN() on integer field: %q", fieldName)
} else if f := e.Holder.Field(index, fieldName); f != nil && (f.Type() == FieldTypeInt || f.Type() == FieldTypeDecimal || f.Type() == FieldTypeTimestamp) {
return nil, fmt.Errorf("cannot compute TopN() on integer, decimal, or timestamp field: %q", fieldName)
}
attrName, _ := c.Args["attrName"].(string)
@ -3001,7 +3003,7 @@ func (e *executor) executeGroupBy(ctx context.Context, qcx *Qcx, index string, c
return nil, newNotFoundError(ErrFieldNotFound, fieldName)
}
switch f.Type() {
case FieldTypeInt:
case FieldTypeInt, FieldTypeTimestamp:
bases[i] = f.bsiGroup(f.name).Base
}
@ -4420,7 +4422,7 @@ func (e *executor) executeExtractShard(ctx context.Context, qcx *Qcx, index stri
}
}
case FieldTypeInt, FieldTypeDecimal:
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
// Handle an int/decimal field by rotating a BSI matrix.
// Extract the BSI view fragment.
@ -5128,8 +5130,8 @@ func (e *executor) executeClearBit(ctx context.Context, qcx *Qcx, index string,
return false, newNotFoundError(ErrFieldNotFound, fieldName)
}
// Int field.
if f.Type() == FieldTypeInt || f.Type() == FieldTypeDecimal {
// BSI field
if f.Type() == FieldTypeInt || f.Type() == FieldTypeDecimal || f.Type() == FieldTypeTimestamp {
return e.executeClearValueField(ctx, qcx, index, c, f, colID, opt)
}
@ -5457,8 +5459,8 @@ func (e *executor) executeSet(ctx context.Context, qcx *Qcx, index string, c *pq
}
switch f.Type() {
case FieldTypeInt, FieldTypeDecimal:
// Int or Decimal field.
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
// Fetch field
v, ok := c.Arg(fieldName)
if !ok {
return false, fmt.Errorf("Set() row argument '%v' required", rowLabel)
@ -6518,6 +6520,7 @@ func fieldValidateValue(f *Field, val interface{}) error {
case int64:
case float64:
case pql.Decimal:
case time.Time:
case []interface{}:
for _, v := range v {
if err := fieldValidateValue(f, v); err != nil {
@ -6570,6 +6573,12 @@ func fieldValidateValue(f *Field, val interface{}) error {
default:
return errors.Errorf("invalid value %v for decimal field %q", v, f.Name())
}
case FieldTypeTimestamp:
switch v := val.(type) {
case time.Time:
default:
return errors.Errorf("invalid value %v for timestamp field %q", v, f.Name())
}
default:
return errors.Errorf("unsupported type %s of field %q", f.Type(), f.Name())
}
@ -6631,9 +6640,9 @@ func (e *executor) translateCall(c *pql.Call, index string, columnKeys map[strin
}
if c.Name == "Row" {
switch f.Type() {
case FieldTypeInt, FieldTypeDecimal:
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
if _, ok := arg.(*pql.Condition); !ok {
// This is workaround to support pql.ASSIGN ('=') as condition ('==') for int and decimal fields.
// This is workaround to support pql.ASSIGN ('=') as condition ('==') for BSI fields.
arg = &pql.Condition{
Op: pql.EQ,
Value: arg,
@ -7384,6 +7393,18 @@ func (e *executor) translateResult(ctx context.Context, index string, idx *Index
return nil, errors.Errorf("BSI field %q has too many values: %v", field.Name(), ids)
}
}
case FieldTypeTimestamp:
datatype = "timestamp"
mapper = func(ids []uint64) (_ interface{}, err error) {
switch len(ids) {
case 0:
return nil, nil
case 1:
return time.Unix(0, int64(ids[0])*int64(TimeUnitNano(field.Options().TimeUnit))).UTC(), nil
default:
return nil, errors.Errorf("BSI field %q has too many values: %v", field.Name(), ids)
}
}
default:
return nil, errors.Errorf("field type %q not yet supported", typ)
}
@ -7655,17 +7676,19 @@ func (sr *SignedRow) union(other SignedRow) SignedRow {
// ValCount represents a grouping of sum & count for Sum() and Average() calls. Also Min, Max....
type ValCount struct {
Val int64 `json:"value"`
FloatVal float64 `json:"floatValue"`
DecimalVal *pql.Decimal `json:"decimalValue"`
Count int64 `json:"count"`
Val int64 `json:"value"`
FloatVal float64 `json:"floatValue"`
DecimalVal *pql.Decimal `json:"decimalValue"`
TimestampVal time.Time `json:"timestampValue"`
Count int64 `json:"count"`
}
func (v *ValCount) Clone() (r *ValCount) {
r = &ValCount{
Val: v.Val,
FloatVal: v.FloatVal,
Count: v.Count,
Val: v.Val,
FloatVal: v.FloatVal,
TimestampVal: v.TimestampVal,
Count: v.Count,
}
if v.DecimalVal != nil {
r.DecimalVal = v.DecimalVal.Clone()
@ -7709,6 +7732,19 @@ func (v ValCount) ToRows(callback func(*proto.RowResponse) error) error {
}}); err != nil {
return errors.Wrap(err, "calling callback")
}
} else if !v.TimestampVal.IsZero() {
ci = []*proto.ColumnInfo{
{Name: "value", Datatype: "string"},
{Name: "count", Datatype: "int64"},
}
if err := callback(&proto.RowResponse{
Headers: ci,
Columns: []*proto.ColumnResponse{
&proto.ColumnResponse{ColumnVal: &proto.ColumnResponse_StringVal{StringVal: v.TimestampVal.Format(time.RFC3339Nano)}},
&proto.ColumnResponse{ColumnVal: &proto.ColumnResponse_Int64Val{Int64Val: v.Count}},
}}); err != nil {
return errors.Wrap(err, "calling callback")
}
} else {
ci = []*proto.ColumnInfo{
{Name: "value", Datatype: "int64"},
@ -7955,12 +7991,12 @@ func newGroupByIterator(executor *executor, qcx *Qcx, rowIDs []RowIDs, children
} else {
viewName = viewStandard
}
case FieldTypeInt:
case FieldTypeInt, FieldTypeTimestamp:
viewName = viewBSIGroupPrefix + fieldName
default: // FieldTypeDecimal
return nil, errors.Errorf("%s call must have field of one of types: %s",
call.Name, strings.Join([]string{FieldTypeSet, FieldTypeTime, FieldTypeMutex, FieldTypeBool, FieldTypeInt}, ","))
call.Name, strings.Join([]string{FieldTypeSet, FieldTypeTime, FieldTypeMutex, FieldTypeBool, FieldTypeInt, FieldTypeTimestamp}, ","))
}
filters := []roaring.BitmapFilter{}
@ -8229,6 +8265,13 @@ func getScaledInt(f *Field, v interface{}) (int64, error) {
default:
return 0, errors.Errorf("unexpected decimal value type %T, val %v", tv, tv)
}
} else if opt.Type == FieldTypeTimestamp {
switch tv := v.(type) {
case time.Time:
value = tv.UnixNano()
default:
return 0, errors.Errorf("unexpected timestamp value type %T, val %v", tv, tv)
}
} else {
switch tv := v.(type) {
case int64:

View file

@ -997,6 +997,51 @@ func TestExecutor_Execute_SetValue(t *testing.T) {
}
})
})
t.Run("Timestamp", func(t *testing.T) {
c := test.MustRunCluster(t, 1)
defer c.Close()
hldr := c.GetHolder(0)
// Create fields.
index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
if _, err := index.CreateFieldIfNotExists("f", pilosa.OptFieldTypeTimestamp(pilosa.MinTimestamp, pilosa.MaxTimestamp, pilosa.TimeUnitSeconds)); err != nil {
t.Fatal(err)
} else if _, err := index.CreateFieldIfNotExists("xxx", pilosa.OptFieldTypeDefault()); err != nil {
t.Fatal(err)
}
// Set bsiGroup values.
if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set(10, f='2000-01-01T00:00:00.000000000Z')`}); err != nil {
t.Fatal(err)
} else if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set(100, f='2000-01-02T00:00:00Z')`}); err != nil {
t.Fatal(err)
}
// Obtain transaction.
idx := index.Index
shard := uint64(0)
tx := idx.Txf().NewTx(pilosa.Txo{Write: !writable, Index: idx, Shard: shard})
defer tx.Rollback()
f := hldr.Field("i", "f")
if value, exists, err := f.Value(tx, 10); err != nil {
t.Fatal(err)
} else if !exists {
t.Fatal("expected value to exist")
} else if value != time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).UnixNano() {
t.Fatalf("unexpected value: %v", value)
}
if value, exists, err := f.Value(tx, 100); err != nil {
t.Fatal(err)
} else if !exists {
t.Fatal("expected value to exist")
} else if value != time.Date(2000, time.January, 2, 0, 0, 0, 0, time.UTC).UnixNano() {
t.Fatalf("unexpected value: %v", value)
}
})
}
// Ensure a SetRowAttrs() query can be executed.
@ -1336,7 +1381,7 @@ func TestExecutor_Execute_TopN(t *testing.T) {
t.Fatal(err)
} else if _, err := idx.CreateField("f", pilosa.OptFieldTypeInt(0, 100)); err != nil {
t.Fatal(err)
} else if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `TopN(f, n=2)`}); err == nil || !strings.Contains(err.Error(), `finding top results: mapping on primary node: cannot compute TopN() on integer field: "f"`) {
} else if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `TopN(f, n=2)`}); err == nil || !strings.Contains(err.Error(), `finding top results: mapping on primary node: cannot compute TopN() on integer, decimal, or timestamp field: "f"`) {
t.Fatalf("unexpected error: %v", err)
}
})
@ -1741,6 +1786,95 @@ func TestExecutor_Execute_MinMax(t *testing.T) {
})
}
})
t.Run("Timestamp", func(t *testing.T) {
c := test.MustRunCluster(t, 1)
defer c.Close()
hldr := c.GetHolder(0)
idx, err := hldr.CreateIndex("i", pilosa.IndexOptions{})
if err != nil {
t.Fatal(err)
}
tests := []struct {
min time.Time
max time.Time
set time.Time
}{
{
time.Date(2000, time.January, 10, 0, 0, 0, 0, time.UTC),
time.Date(2000, time.January, 20, 0, 0, 0, 0, time.UTC),
time.Date(2000, time.January, 11, 0, 0, 0, 0, time.UTC),
},
}
for i, test := range tests {
fld := fmt.Sprintf("f%d", i)
t.Run("MinMaxField_"+fld, func(t *testing.T) {
if _, err := idx.CreateField(fld, pilosa.OptFieldTypeTimestamp(test.min, test.max, pilosa.TimeUnitSeconds)); err != nil {
t.Fatal(err)
} else if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: fmt.Sprintf(`Set(10, %s="%s")`, fld, test.set.Format(time.RFC3339))}); err != nil {
t.Fatal(err)
}
var pql string
t.Run("Min", func(t *testing.T) {
pql = fmt.Sprintf(`Min(field=%s)`, fld)
if result, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: pql}); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(result.Results[0], pilosa.ValCount{TimestampVal: test.set, Count: 1}) {
t.Fatalf("unexpected min result, test %d: %s", i, spew.Sdump(result))
}
})
t.Run("Max", func(t *testing.T) {
pql = fmt.Sprintf(`Max(field=%s)`, fld)
if result, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: pql}); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(result.Results[0], pilosa.ValCount{TimestampVal: test.set, Count: 1}) {
t.Fatalf("unexpected max result, test %d: %s", i, spew.Sdump(result))
}
})
t.Run("Min", func(t *testing.T) {
pql = fmt.Sprintf(`Min(field="%s")`, fld)
if result, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: pql}); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(result.Results[0], pilosa.ValCount{TimestampVal: test.set, Count: 1}) {
t.Fatalf("unexpected min result, test %d: %s", i, spew.Sdump(result))
}
})
t.Run("Max", func(t *testing.T) {
pql = fmt.Sprintf(`Max(field="%s")`, fld)
if result, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: pql}); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(result.Results[0], pilosa.ValCount{TimestampVal: test.set, Count: 1}) {
t.Fatalf("unexpected max result, test %d: %s", i, spew.Sdump(result))
}
})
t.Run("Min", func(t *testing.T) {
pql = fmt.Sprintf(`Min(%s)`, fld)
if result, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: pql}); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(result.Results[0], pilosa.ValCount{TimestampVal: test.set, Count: 1}) {
t.Fatalf("unexpected min result, test %d: %s", i, spew.Sdump(result))
}
})
t.Run("Max", func(t *testing.T) {
pql = fmt.Sprintf(`Max(%s)`, fld)
if result, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: pql}); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(result.Results[0], pilosa.ValCount{TimestampVal: test.set, Count: 1}) {
t.Fatalf("unexpected max result, test %d: %s", i, spew.Sdump(result))
}
})
})
}
})
})
t.Run("ColumnID", func(t *testing.T) {

127
field.go
View file

@ -55,12 +55,13 @@ const (
// Field types.
const (
FieldTypeSet = "set"
FieldTypeInt = "int"
FieldTypeTime = "time"
FieldTypeMutex = "mutex"
FieldTypeBool = "bool"
FieldTypeDecimal = "decimal"
FieldTypeSet = "set"
FieldTypeInt = "int"
FieldTypeTime = "time"
FieldTypeMutex = "mutex"
FieldTypeBool = "bool"
FieldTypeDecimal = "decimal"
FieldTypeTimestamp = "timestamp"
)
type protected struct {
@ -204,6 +205,33 @@ func OptFieldTypeInt(min, max int64) FieldOption {
}
}
// OptFieldTypeTimestamp is a functional option on FieldOptions
// used to specify the field as being type `timestamp` and to
// provide any respective configuration values.
func OptFieldTypeTimestamp(min, max time.Time, timeUnit string) FieldOption {
return func(fo *FieldOptions) error {
minNano := min.UnixNano()
maxNano := max.UnixNano()
if fo.Type != "" {
return errors.Errorf("field type is already set to: %s", fo.Type)
}
if timeUnit == "" {
return errors.Errorf("time unit required for timestamp field")
} else if !IsValidTimeUnit(timeUnit) {
return errors.Errorf("invalid time unit: %q", fo.TimeUnit)
}
if min.After(max) {
return errors.New("timestamp field min cannot be greater than max")
}
fo.Type = FieldTypeTimestamp
fo.TimeUnit = timeUnit
fo.Min = pql.NewDecimal(minNano, 0)
fo.Max = pql.NewDecimal(maxNano, 0)
fo.Base = bsiBase(minNano, maxNano)
return nil
}
}
// OptFieldTypeDecimal is a functional option for creating a `decimal` field.
// Unless we decide to expand the range of supported values, `scale` is
// restricted to the range [0,19]. This supports anything from:
@ -797,7 +825,7 @@ func (f *Field) applyOptions(opt FieldOptions) error {
f.options.TimeQuantum = ""
f.options.Keys = opt.Keys
f.options.ForeignIndex = opt.ForeignIndex
case FieldTypeInt, FieldTypeDecimal:
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
f.options.Type = opt.Type
f.options.CacheType = CacheTypeNone
f.options.CacheSize = 0
@ -806,6 +834,7 @@ func (f *Field) applyOptions(opt FieldOptions) error {
f.options.Base = opt.Base
f.options.Scale = opt.Scale
f.options.BitDepth = opt.BitDepth
f.options.TimeUnit = opt.TimeUnit
f.options.TimeQuantum = ""
f.options.Keys = opt.Keys
f.options.ForeignIndex = opt.ForeignIndex
@ -818,6 +847,7 @@ func (f *Field) applyOptions(opt FieldOptions) error {
Max: opt.Max.ToInt64(opt.Scale),
Base: opt.Base,
Scale: opt.Scale,
TimeUnit: opt.TimeUnit,
BitDepth: opt.BitDepth,
}
// Validate and create bsiGroup.
@ -1386,6 +1416,8 @@ func (f *Field) MaxForShard(tx Tx, shard uint64, filter *Row) (ValCount, error)
if f.Options().Type == FieldTypeDecimal {
dec := pql.NewDecimal(max+bsig.Base, bsig.Scale)
valCount.DecimalVal = &dec
} else if f.Options().Type == FieldTypeTimestamp {
valCount.TimestampVal = time.Unix(0, (max + bsig.Base)).UTC()
} else {
valCount.Val = max + bsig.Base
}
@ -1430,6 +1462,8 @@ func (f *Field) MinForShard(tx Tx, shard uint64, filter *Row) (ValCount, error)
if f.Options().Type == FieldTypeDecimal {
dec := pql.NewDecimal(min+bsig.Base, bsig.Scale)
valCount.DecimalVal = &dec
} else if f.Options().Type == FieldTypeTimestamp {
valCount.TimestampVal = time.Unix(0, (min + bsig.Base)).UTC()
} else {
valCount.Val = min + bsig.Base
}
@ -1708,10 +1742,10 @@ func (f *Field) importRoaringOverwrite(ctx context.Context, tx Tx, data []byte,
return err
}
// If field is int or decimal, then we need to update field.options.BitDepth
// and bsiGroup.BitDepth based on the imported data.
// If field is int, decimal, or timestamp, then we need to update
// field.options.BitDepth and bsiGroup.BitDepth based on the imported data.
switch f.Options().Type {
case FieldTypeInt, FieldTypeDecimal:
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
frag.mu.Lock()
maxRowID, _, err := frag.maxRow(tx, nil)
frag.mu.Unlock()
@ -1772,6 +1806,7 @@ type FieldOptions struct {
CacheSize uint32 `json:"cacheSize,omitempty"`
CacheType string `json:"cacheType,omitempty"`
Type string `json:"type,omitempty"`
TimeUnit string `json:"timeUnit,omitempty"`
TimeQuantum TimeQuantum `json:"timeQuantum,omitempty"`
ForeignIndex string `json:"foreignIndex"`
}
@ -1794,6 +1829,9 @@ func newFieldOptions(opts ...FieldOption) (*FieldOptions, error) {
case FieldTypeDecimal:
return nil, ErrDecimalFieldWithKeys
case FieldTypeTimestamp:
return nil, ErrTimestampFieldWithKeys
}
}
@ -1867,6 +1905,26 @@ func (o *FieldOptions) MarshalJSON() ([]byte, error) {
o.Max,
o.Keys,
})
case FieldTypeTimestamp:
return json.Marshal(struct {
Type string `json:"type"`
Base int64 `json:"base"`
BitDepth uint64 `json:"bitDepth"`
Min pql.Decimal `json:"min"`
Max pql.Decimal `json:"max"`
Keys bool `json:"keys"`
TimeUnit string `json:"timeUnit"`
ForeignIndex string `json:"foreignIndex"`
}{
o.Type,
o.Base,
o.BitDepth,
o.Min,
o.Max,
o.Keys,
o.TimeUnit,
o.ForeignIndex,
})
case FieldTypeTime:
return json.Marshal(struct {
Type string `json:"type"`
@ -1901,6 +1959,16 @@ func (o *FieldOptions) MarshalJSON() ([]byte, error) {
return nil, errors.New("invalid field type")
}
// MinTimestamp returns the minimum value for a timestamp field.
func (o FieldOptions) MinTimestamp() time.Time {
return time.Unix(0, o.Min.ToInt64(0)*int64(TimeUnitNano(o.TimeUnit)))
}
// MaxTimestamp returns the maxnimum value for a timestamp field.
func (o FieldOptions) MaxTimestamp() time.Time {
return time.Unix(0, o.Max.ToInt64(0)*int64(TimeUnitNano(o.TimeUnit)))
}
// List of bsiGroup types.
const (
bsiGroupTypeInt = "int"
@ -1935,6 +2003,7 @@ type bsiGroup struct {
Max int64 `json:"max,omitempty"`
Base int64 `json:"base,omitempty"`
Scale int64 `json:"scale,omitempty"`
TimeUnit string `json:"timeUnit,omitempty"`
BitDepth uint64 `json:"bitDepth,omitempty"`
}
@ -2064,3 +2133,41 @@ func (f *Field) persistView(ctx context.Context, cvm *CreateViewMessage) error {
return f.schemator.CreateView(ctx, cvm.Index, cvm.Field, cvm.View)
}
// Timestamp field range.
var (
MinTimestamp = time.Unix(-1<<42, 0).UTC()
MaxTimestamp = time.Unix(1<<42, 0).UTC()
)
// List of time units.
const (
TimeUnitSeconds = "s"
TimeUnitMilliseconds = "ms"
TimeUnitMicroseconds = "µs"
TimeUnitNanoseconds = "ns"
)
// IsValidTimeUnit returns true if unit is valid.
func IsValidTimeUnit(unit string) bool {
switch unit {
case TimeUnitSeconds, TimeUnitMilliseconds, TimeUnitMicroseconds, TimeUnitNanoseconds:
return true
default:
return false
}
}
// TimeUnitNano returns the number of nanoseconds in unit.
func TimeUnitNano(unit string) int64 {
switch unit {
case TimeUnitSeconds:
return int64(time.Second)
case TimeUnitMilliseconds:
return int64(time.Millisecond)
case TimeUnitMicroseconds:
return int64(time.Microsecond)
default:
return int64(time.Nanosecond)
}
}

View file

@ -3644,7 +3644,7 @@ func (s *fragmentSyncer) syncFragment() error {
// to continue processing int/decimal fields.
if nodes[0].ID != s.Node.ID {
switch s.FieldType {
case FieldTypeInt, FieldTypeDecimal:
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
return nil
}
}
@ -3721,7 +3721,7 @@ func (s *fragmentSyncer) syncFragment() error {
s.Fragment.holder.Logger.Debugf("sync block from primary: index='%v' field='%v' view='%v' shard='%v' id=%d", s.Fragment.index(), s.Fragment.field(), s.Fragment.view(), s.Fragment.shard, blockID)
switch s.FieldType {
case FieldTypeInt, FieldTypeDecimal:
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
// Synchronize block from the primary replica.
if err := s.syncBlockFromPrimary(blockID); err != nil {
return fmt.Errorf("sync block from primary: id=%d, err=%s", blockID, err)

View file

@ -16,6 +16,7 @@ package pilosa
import (
"encoding/json"
"time"
"github.com/pilosa/pilosa/v2/tracing"
"github.com/pkg/errors"
@ -128,13 +129,14 @@ type ImportValueRequest struct {
FieldCreatedAt int64
// if Shard is MaxUint64 (an impossible shard value), this
// indicates that the column IDs may come from multiple shards.
Shard uint64
ColumnIDs []uint64 // e.g. weather stationID
ColumnKeys []string
Values []int64 // e.g. temperature, humidity, barometric pressure
FloatValues []float64
StringValues []string
Clear bool
Shard uint64
ColumnIDs []uint64 // e.g. weather stationID
ColumnKeys []string
Values []int64 // e.g. temperature, humidity, barometric pressure
FloatValues []float64
TimestampValues []time.Time
StringValues []string
Clear bool
}
// AtomicRecord applies all its Ivr and Ivr atomically, in a Tx.
@ -158,6 +160,8 @@ func (ivr *ImportValueRequest) Swap(i, j int) {
ivr.Values[i], ivr.Values[j] = ivr.Values[j], ivr.Values[i]
} else if len(ivr.FloatValues) > 0 {
ivr.FloatValues[i], ivr.FloatValues[j] = ivr.FloatValues[j], ivr.FloatValues[i]
} else if len(ivr.TimestampValues) > 0 {
ivr.TimestampValues[i], ivr.TimestampValues[j] = ivr.TimestampValues[j], ivr.TimestampValues[i]
} else if len(ivr.StringValues) > 0 {
ivr.StringValues[i], ivr.StringValues[j] = ivr.StringValues[j], ivr.StringValues[i]
}
@ -183,6 +187,9 @@ func (ivr *ImportValueRequest) ValidateWithTimestamp(indexCreatedAt, fieldCreate
if len(ivr.FloatValues) != 0 {
valueSetCount++
}
if len(ivr.TimestampValues) != 0 {
valueSetCount++
}
if len(ivr.StringValues) != 0 {
valueSetCount++
}

View file

@ -1340,6 +1340,20 @@ func (h *Handler) handlePostField(w http.ResponseWriter, r *http.Request) {
}
}
fos = append(fos, pilosa.OptFieldTypeDecimal(scale, minmax...))
case pilosa.FieldTypeTimestamp:
if req.Options.Min == nil {
min := pql.NewDecimal(pilosa.MinTimestamp.UnixNano()/pilosa.TimeUnitNano(*req.Options.TimeUnit), 0)
req.Options.Min = &min
}
if req.Options.Max == nil {
max := pql.NewDecimal(pilosa.MaxTimestamp.UnixNano()/pilosa.TimeUnitNano(*req.Options.TimeUnit), 0)
req.Options.Max = &max
}
fos = append(fos, pilosa.OptFieldTypeTimestamp(
time.Unix(0, req.Options.Min.ToInt64(0)*pilosa.TimeUnitNano(*req.Options.TimeUnit)).UTC(),
time.Unix(0, req.Options.Max.ToInt64(0)*pilosa.TimeUnitNano(*req.Options.TimeUnit)).UTC(),
*req.Options.TimeUnit,
))
case pilosa.FieldTypeTime:
fos = append(fos, pilosa.OptFieldTypeTime(*req.Options.TimeQuantum, req.Options.NoStandardView))
case pilosa.FieldTypeMutex:
@ -1384,6 +1398,7 @@ type fieldOptions struct {
Min *pql.Decimal `json:"min,omitempty"`
Max *pql.Decimal `json:"max,omitempty"`
Scale *int64 `json:"scale,omitempty"`
TimeUnit *string `json:"timeUnit,omitempty"`
TimeQuantum *pilosa.TimeQuantum `json:"timeQuantum,omitempty"`
Keys *bool `json:"keys,omitempty"`
NoStandardView bool `json:"noStandardView,omitempty"`
@ -1423,8 +1438,8 @@ func (o *fieldOptions) validate() error {
return pilosa.NewBadRequestError(errors.New("cacheSize does not apply to field type int"))
} else if o.TimeQuantum != nil {
return pilosa.NewBadRequestError(errors.New("timeQuantum does not apply to field type int"))
} else if o.ForeignIndex != nil && o.Type == pilosa.FieldTypeDecimal {
return pilosa.NewBadRequestError(errors.New("decimal field cannot be a foreign key"))
} else if o.ForeignIndex != nil {
return pilosa.NewBadRequestError(errors.New("int field cannot be a foreign key"))
}
case pilosa.FieldTypeDecimal:
if o.Scale == nil {
@ -1438,6 +1453,20 @@ func (o *fieldOptions) validate() error {
} else if o.ForeignIndex != nil && o.Type == pilosa.FieldTypeDecimal {
return pilosa.NewBadRequestError(errors.New("decimal field cannot be a foreign key"))
}
case pilosa.FieldTypeTimestamp:
if o.TimeUnit == nil {
return pilosa.NewBadRequestError(errors.New("timestamp field requires a timeUnit argument"))
} else if !pilosa.IsValidTimeUnit(*o.TimeUnit) {
return pilosa.NewBadRequestError(errors.New("invalid timeUnit argument"))
} else if o.CacheType != nil {
return pilosa.NewBadRequestError(errors.New("cacheType does not apply to field type timestamp"))
} else if o.CacheSize != nil {
return pilosa.NewBadRequestError(errors.New("cacheSize does not apply to field type timestamp"))
} else if o.TimeQuantum != nil {
return pilosa.NewBadRequestError(errors.New("timeQuantum does not apply to field type timestamp"))
} else if o.ForeignIndex != nil {
return pilosa.NewBadRequestError(errors.New("timestamp field cannot be a foreign key"))
}
case pilosa.FieldTypeTime:
if o.CacheType != nil {
return pilosa.NewBadRequestError(errors.New("cacheType does not apply to field type time"))
@ -2384,7 +2413,7 @@ func (h *Handler) handlePostImport(w http.ResponseWriter, r *http.Request) {
return
}
// Unmarshal request based on field type.
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeDecimal {
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeDecimal || field.Type() == pilosa.FieldTypeTimestamp {
// Field type: Int
// Marshal into request object.
req := &pilosa.ImportValueRequest{}

View file

@ -430,7 +430,7 @@ func (i *Index) openExistenceField() error {
func (i *Index) setFieldBitDepths() error {
for name, f := range i.fields {
switch f.Type() {
case FieldTypeInt, FieldTypeDecimal:
case FieldTypeInt, FieldTypeDecimal, FieldTypeTimestamp:
// pass
default:
continue

View file

@ -89,7 +89,7 @@ func TestIndex_CreateField(t *testing.T) {
// Ensure field can include range columns.
t.Run("BSIFields", func(t *testing.T) {
t.Run("OK", func(t *testing.T) {
t.Run("Int", func(t *testing.T) {
index := test.MustOpenIndex(t)
defer index.Close()
@ -108,6 +108,25 @@ func TestIndex_CreateField(t *testing.T) {
}
})
t.Run("Timestamp", func(t *testing.T) {
index := test.MustOpenIndex(t)
defer index.Close()
// Create field with schema and verify it exists.
if f, err := index.CreateField("f", pilosa.OptFieldTypeTimestamp(pilosa.MinTimestamp, pilosa.MaxTimestamp, pilosa.TimeUnitSeconds)); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(f.Type(), pilosa.FieldTypeTimestamp) {
t.Fatalf("unexpected type: %#v", f.Type())
}
// Reopen the index & verify the fields are loaded.
if err := index.Reopen(); err != nil {
t.Fatal(err)
} else if f := index.Field("f"); !reflect.DeepEqual(f.Type(), pilosa.FieldTypeTimestamp) {
t.Fatalf("unexpected type after reopen: %#v", f.Type())
}
})
// TODO: These errors don't apply here. Instead, we need these tests
// on field creation FieldOptions validation.
/*

View file

@ -87,8 +87,9 @@ var (
ErrFieldsArgumentRequired = errors.New("fields argument required")
ErrExpectedFieldListArgument = errors.New("expected field list argument")
ErrIntFieldWithKeys = errors.New("int field cannot be created with 'keys=true' option")
ErrDecimalFieldWithKeys = errors.New("decimal field cannot be created with 'keys=true' option")
ErrIntFieldWithKeys = errors.New("int field cannot be created with 'keys=true' option")
ErrDecimalFieldWithKeys = errors.New("decimal field cannot be created with 'keys=true' option")
ErrTimestampFieldWithKeys = errors.New("timestamp field cannot be created with 'keys=true' option")
)
// apiMethodNotAllowedError wraps an error value indicating that a particular

View file

@ -202,6 +202,38 @@ func (q *Query) addNumVal(val string) {
elem.lastCond = ILLEGAL
}
func (q *Query) addTimestampVal(val string) {
elem := q.lastCallStackElem()
if elem == nil || elem.lastField == "" {
panic(fmt.Sprintf("addTimestampVal called with '%s' when lastField is empty", val))
}
tsval := parseTimestamp(val)
if elem.inList {
if elem.lastCond != ILLEGAL {
list := elem.call.Args[elem.lastField].(*Condition).Value.([]interface{})
elem.call.Args[elem.lastField] = &Condition{
Op: elem.lastCond,
Value: append(list, tsval),
}
} else {
list := elem.call.Args[elem.lastField].([]interface{})
elem.call.Args[elem.lastField] = append(list, tsval)
}
return
} else if elem.lastCond != ILLEGAL {
q.validateArgField(elem) // case 3
elem.call.Args[elem.lastField] = &Condition{
Op: elem.lastCond,
Value: tsval,
}
} else {
q.validateArgField(elem) // case 4
elem.call.Args[elem.lastField] = tsval
}
elem.lastField = ""
elem.lastCond = ILLEGAL
}
func (q *Query) startList() {
elem := q.lastCallStackElem()
q.validateArgField(elem) // case 5
@ -1109,3 +1141,11 @@ func parseNum(val string) interface{} {
}
return ival
}
func parseTimestamp(val string) time.Time {
tsval, err := time.Parse(time.RFC3339Nano, val)
if err != nil {
panic(fmt.Sprintf("%s: %s", invalidTimestampError, err))
}
return tsval
}

View file

@ -31,6 +31,7 @@ const timeFormat = "2006-01-02T15:04"
// error strings in the parser
const duplicateArgErrorMessage = "duplicate argument provided"
const intOutOfRangeError = "integer is not in signed 64-bit range"
const invalidTimestampError = "string is not a timestamp"
// parser represents a parser for the PQL language.
type parser struct {

View file

@ -6,7 +6,7 @@ type PQL Peg {
# All input queries consist of a sequence of calls, at the top level.
Calls <- sp (Call sp)* !.
Call <- "Set" {p.startCall("Set")} open col comma args (comma timestamp)? close {p.endCall()}
Call <- "Set" {p.startCall("Set")} open col comma args (comma time)? close {p.endCall()}
/ "SetRowAttrs" {p.startCall("SetRowAttrs")} open posfield comma row comma args close {p.endCall()}
/ "SetColumnAttrs" {p.startCall("SetColumnAttrs")} open col comma args close {p.endCall()}
/ "Clear" {p.startCall("Clear")} open col comma args close {p.endCall()}
@ -19,7 +19,7 @@ Call <- "Set" {p.startCall("Set")} open col comma args (comma timestamp)? close
/ "Min" {p.startCall("Min")} open posfield (comma allargs)? close {p.endCall()}
/ "Max" {p.startCall("Max")} open posfield (comma allargs)? close {p.endCall()}
/ "Sum" {p.startCall("Sum")} open posfield (comma allargs)? close {p.endCall()}
/ "Range" {p.startCall("Range")} open field eq value comma 'from='? {p.addField("from")} timestampfmt {p.addVal(text)} comma 'to='? sp {p.addField("to")} timestampfmt {p.addVal(text)} close {p.endCall()}
/ "Range" {p.startCall("Range")} open field eq value comma 'from='? {p.addField("from")} timefmt {p.addVal(text)} comma 'to='? sp {p.addField("to")} timefmt {p.addVal(text)} close {p.endCall()}
/ < IDENT > { p.startCall(text) } open allargs comma? close { p.endCall() }
allargs <- Call (comma Call)* (comma args)? / args / sp
args <- arg (comma args)? sp
@ -45,7 +45,8 @@ items <- item (comma items)?
item <- 'null' &(comma / close) { p.addVal(nil) }
/ 'true' &(comma / close) { p.addVal(true) }
/ 'false' &(comma / close) { p.addVal(false) }
/ timestampfmt { p.addVal(text) }
/ timefmt { p.addVal(text) }
/ timestampfmt { p.addTimestampVal(text) }
/ < decimal > { p.addNumVal(text) }
/ < IDENT > { p.startCall(text) } open allargs comma? close { p.addVal(p.endCall()) }
/ < ([[A-Z]] / [0-9] / '-' / '_' / ':')+ > { p.addVal(text) }
@ -79,6 +80,12 @@ signedDigits <- '-'? digits
decimal <- signedDigits ('.' digits?)?
/ '-'? '.' digits
timestampbasicfmt <- [0-9][0-9][0-9][0-9]'-'[01][0-9]'-'[0-3][0-9]'T'[0-9][0-9]':'[0-9][0-9]
tz <- 'Z' / '-' [0-9][0-9]':'[0-9][0-9] / '+'[0-9][0-9]':'[0-9][0-9]
iso8601 <- [0-9][0-9][0-9][0-9]'-'[01][0-9]'-'[0-3][0-9]'T'[0-9][0-9]':'[0-9][0-9]':'[0-9][0-9] <tz>
iso8601nano <- [0-9][0-9][0-9][0-9]'-'[01][0-9]'-'[0-3][0-9]'T'[0-9][0-9]':'[0-9][0-9]':'[0-9][0-9]'.'[0-9]+ <tz>
timestampbasicfmt <- iso8601nano / iso8601
timestampfmt <- '"' <timestampbasicfmt> '"' / '\'' <timestampbasicfmt> '\'' / <timestampbasicfmt>
timestamp <- <timestampfmt> {p.addPosStr("_timestamp", text)}
timebasicfmt <- [0-9][0-9][0-9][0-9]'-'[01][0-9]'-'[0-3][0-9]'T'[0-9][0-9]':'[0-9][0-9]
timefmt <- '"' <timebasicfmt> '"' / '\'' <timebasicfmt> '\'' / <timebasicfmt>
time <- <timefmt> {p.addPosStr("_timestamp", text)}

File diff suppressed because it is too large Load diff

View file

@ -142,6 +142,22 @@ func TestPEGWorking(t *testing.T) {
name: "SetTimestamp",
input: "Set(1, a=4, 2017-04-03T19:34)",
ncalls: 1},
{
name: "SetTimestampField",
input: "Set(1, a='2017-04-03T19:34:00Z')",
ncalls: 1},
{
name: "SetTimestampTZField",
input: "Set(1, a='2017-04-03T19:34:00-07:00')",
ncalls: 1},
{
name: "SetTimestampTZField",
input: "Set(1, a='2017-04-03T19:34:00+07:00')",
ncalls: 1},
{
name: "SetTimestampNanoField",
input: "Set(1, a='2017-04-03T19:34:00.000000Z')",
ncalls: 1},
{
name: "Union()",
input: "Union()",

View file

@ -19,6 +19,7 @@ import (
"reflect"
"strconv"
"strings"
"time"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/pql"
@ -251,7 +252,7 @@ func extractWhere(index *pilosa.Index, expr sqlparser.Expr) (string, error) {
}
switch field.Type() {
case pilosa.FieldTypeInt, pilosa.FieldTypeDecimal:
case pilosa.FieldTypeInt, pilosa.FieldTypeDecimal, pilosa.FieldTypeTimestamp:
switch op {
case "=":
return Equals(field.Name(), val), nil
@ -365,6 +366,16 @@ func extractWhere(index *pilosa.Index, expr sqlparser.Expr) (string, error) {
return "", err
}
return Between(field.Name(), fromNum, toNum), nil
case pilosa.FieldTypeTimestamp:
fromTime, err := extractTimestamp(e.From)
if err != nil {
return "", err
}
toTime, err := extractTimestamp(e.To)
if err != nil {
return "", err
}
return Between(field.Name(), fromTime, toTime), nil
default:
return "", errors.New("only int and float64 fields are supported")
}
@ -374,13 +385,13 @@ func extractWhere(index *pilosa.Index, expr sqlparser.Expr) (string, error) {
return "", errors.New("left operand must be a column name")
}
field := index.Field(left.Name.String())
if field.Type() == pilosa.FieldTypeInt {
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
if e.Operator == "is not null" {
return NotNull(field.Name()), nil
}
return "", errors.New("only `is not null` is supported for int fields")
return "", fmt.Errorf("only `is not null` is supported for %s fields", field.Type())
}
return "", errors.New("`is` expression is supported only for int fields")
return "", fmt.Errorf("`is` expression is supported only for %s fields", field.Type())
}
return "", errors.New("cannot extract where")
}
@ -523,6 +534,22 @@ func extractFloat(e sqlparser.Expr) (float64, error) {
return num, nil
}
func extractTimestamp(e sqlparser.Expr) (time.Time, error) {
val, err := extractVal(e)
if err != nil {
return time.Time{}, err
}
s, ok := val.(string)
if !ok {
return time.Time{}, errors.New("value must be an ISO 8601-formated timestamp string")
}
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return time.Time{}, errors.New("value must be an ISO 8601-formated timestamp string")
}
return t, nil
}
func extractStr(e sqlparser.Expr) (string, error) {
val, err := extractVal(e)
if err != nil {
@ -961,26 +988,32 @@ func extractWheres(indexes []*pilosa.Index, tbls parseTables, expr sqlparser.Exp
table: pTable,
}
if field.Type() == pilosa.FieldTypeInt {
num, ok := val.(int)
if !ok {
return nil, errors.New("right operand must be a number")
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
if field.Type() == pilosa.FieldTypeInt {
if _, ok := val.(int); !ok {
return nil, errors.New("right operand must be a number")
}
} else { // timestamp
if _, ok := val.(time.Time); !ok {
return nil, errors.New("right operand must be a timestamp")
}
}
switch op {
case "=":
tw.where = Equals(field.Name(), num)
tw.where = Equals(field.Name(), val)
case "<":
tw.where = LT(field.Name(), num)
tw.where = LT(field.Name(), val)
case "<=":
tw.where = LTE(field.Name(), num)
tw.where = LTE(field.Name(), val)
case ">":
tw.where = GT(field.Name(), num)
tw.where = GT(field.Name(), val)
case ">=":
tw.where = GTE(field.Name(), num)
tw.where = GTE(field.Name(), val)
case "<>":
fallthrough
case "!=":
tw.where = NotEquals(field.Name(), num)
tw.where = NotEquals(field.Name(), val)
}
return append(wheres, tw), nil
}
@ -1148,7 +1181,19 @@ func extractWheres(indexes []*pilosa.Index, tbls parseTables, expr sqlparser.Exp
tw.where = Between(field.Name(), fromNum, toNum)
return append(wheres, tw), nil
}
return nil, errors.New("only int fields are supported")
if field.Type() == pilosa.FieldTypeTimestamp {
fromTime, err := extractTimestamp(e.From)
if err != nil {
return nil, err
}
toTime, err := extractTimestamp(e.To)
if err != nil {
return nil, err
}
tw.where = Between(field.Name(), fromTime, toTime)
return append(wheres, tw), nil
}
return nil, errors.New("only int or timestamp fields are supported")
case *sqlparser.IsExpr:
left, ok := e.Expr.(*sqlparser.ColName)
if !ok {
@ -1172,14 +1217,14 @@ func extractWheres(indexes []*pilosa.Index, tbls parseTables, expr sqlparser.Exp
}
field := pTable.index.Field(pCol.name)
if field.Type() == pilosa.FieldTypeInt {
if field.Type() == pilosa.FieldTypeInt || field.Type() == pilosa.FieldTypeTimestamp {
if e.Operator == "is not null" {
tw.where = NotNull(field.Name())
return append(wheres, tw), nil
}
return nil, errors.New("only `is not null` is supported for int fields")
return nil, fmt.Errorf("only `is not null` is supported for %s fields", field.Type())
}
return nil, errors.New("`is` expression is supported only for int fields")
return nil, fmt.Errorf("`is` expression is supported only for %s fields", field.Type())
}
return nil, errors.New("cannot extract where")
}

View file

@ -335,7 +335,7 @@ func (h handlerSelectDistinctFromTable) Apply(stmt *sqlparser.Select, qm QueryMa
// Otherwise, use Rows()
// TODO: ensure this works for all field types (bool, time, etc).
var qo string
if fieldCol.Field.Type() == pilosa.FieldTypeInt {
if fieldCol.Field.Type() == pilosa.FieldTypeInt || fieldCol.Field.Type() == pilosa.FieldTypeTimestamp {
qo = Distinct(fieldCol.Field.Index(), fieldCol.Field.Name())
} else {
if !qm.HasOrderBy() && limit > 0 {

View file

@ -273,7 +273,7 @@ fragLoop:
// flags returns a set of flags for the underlying fragments.
func (v *view) flags() byte {
var flag byte
if v.fieldType == FieldTypeInt || v.fieldType == FieldTypeDecimal {
if v.fieldType == FieldTypeInt || v.fieldType == FieldTypeDecimal || v.fieldType == FieldTypeTimestamp {
flag |= roaringFlagBSIv2
}
return flag