mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-07 09:05:55 +00:00
Merge pull request #1558 from molecula/timestamp
CORE-372: Add timestamp field type support
This commit is contained in:
commit
7e369aeac4
19 changed files with 1731 additions and 838 deletions
50
api_test.go
50
api_test.go
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() != "" {
|
||||
|
|
|
|||
81
executor.go
81
executor.go
|
|
@ -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:
|
||||
|
|
|
|||
136
executor_test.go
136
executor_test.go
|
|
@ -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
127
field.go
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
21
handler.go
21
handler.go
|
|
@ -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++
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
2
index.go
2
index.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
40
pql/ast.go
40
pql/ast.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
17
pql/pql.peg
17
pql/pql.peg
|
|
@ -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)}
|
||||
|
|
|
|||
1926
pql/pql.peg.go
1926
pql/pql.peg.go
File diff suppressed because it is too large
Load diff
|
|
@ -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()",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
2
view.go
2
view.go
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue