mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
Previously, multiple frames with different prefixes were used to separate different data layouts. This included separating standard row/column layouts from inverted column/row layouts as well as storing aggregate information for timestamp data. Unfortunately, this caused frame meta data to be copied between multiple frames and it made it difficult to keep these frames in sync. This commit separates these different physical layouts into `Views`. A `Frame` now has one or more views which represent each layout. Fragments have been moved from under the `Frame` to be contained within the `View`. There are two primary views: - `standard` - `inverse` If a frame has a time quantum, then views are generated for these each of the standard/inverse views. For example a time quantum of `YMDH` for the date `2000-01-02T00:00:00Z` would create the following views: - `standard_2000` - `inverse_2000` - `standard_200001` - `inverse_200001` - `standard_20000102` - `inverse_20000102` From the user's perspective, nothing should change in PQL. Different PQL statements will handle the appropriate view automatically. For example, `Bitmap()` and `Profile()` will fetch using the `standard` view or the `inverse` view, respectively. The `Range()` statement will lookup the appropriate time-based views automatically.
181 lines
4.4 KiB
Go
181 lines
4.4 KiB
Go
package pilosa
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ErrInvalidTimeQuantum is returned when parsing a time quantum.
|
|
var ErrInvalidTimeQuantum = errors.New("invalid time quantum")
|
|
|
|
// TimeQuantum represents a time granularity for time-based bitmaps.
|
|
type TimeQuantum string
|
|
|
|
// HasYear returns true if the quantum contains a 'Y' unit.
|
|
func (q TimeQuantum) HasYear() bool { return strings.ContainsRune(string(q), 'Y') }
|
|
|
|
// HasMonth returns true if the quantum contains a 'M' unit.
|
|
func (q TimeQuantum) HasMonth() bool { return strings.ContainsRune(string(q), 'M') }
|
|
|
|
// HasDay returns true if the quantum contains a 'D' unit.
|
|
func (q TimeQuantum) HasDay() bool { return strings.ContainsRune(string(q), 'D') }
|
|
|
|
// HasHour returns true if the quantum contains a 'H' unit.
|
|
func (q TimeQuantum) HasHour() bool { return strings.ContainsRune(string(q), 'H') }
|
|
|
|
// Valid returns true if q is a valid time quantum value.
|
|
func (q TimeQuantum) Valid() bool {
|
|
switch q {
|
|
case "Y", "YM", "YMD", "YMDH",
|
|
"M", "MD", "MDH",
|
|
"D", "DH",
|
|
"H",
|
|
"":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ParseTimeQuantum parses v into a time quantum.
|
|
func ParseTimeQuantum(v string) (TimeQuantum, error) {
|
|
q := TimeQuantum(strings.ToUpper(v))
|
|
if !q.Valid() {
|
|
return "", ErrInvalidTimeQuantum
|
|
}
|
|
return q, nil
|
|
}
|
|
|
|
// ViewByTimeUnit returns the view name for time with a given quantum unit.
|
|
func ViewByTimeUnit(name string, t time.Time, unit rune) string {
|
|
switch unit {
|
|
case 'Y':
|
|
return fmt.Sprintf("%s_%s", name, t.Format("2006"))
|
|
case 'M':
|
|
return fmt.Sprintf("%s_%s", name, t.Format("200601"))
|
|
case 'D':
|
|
return fmt.Sprintf("%s_%s", name, t.Format("20060102"))
|
|
case 'H':
|
|
return fmt.Sprintf("%s_%s", name, t.Format("2006010215"))
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// ViewsByTime returns a list of views for a given timestamp.
|
|
func ViewsByTime(name string, t time.Time, q TimeQuantum) []string {
|
|
a := make([]string, 0, len(q))
|
|
for _, unit := range q {
|
|
view := ViewByTimeUnit(name, t, unit)
|
|
if view == "" {
|
|
continue
|
|
}
|
|
a = append(a, view)
|
|
}
|
|
return a
|
|
}
|
|
|
|
// ViewsByTimeRange returns a list of views to traverse to query a time range.
|
|
func ViewsByTimeRange(name string, start, end time.Time, q TimeQuantum) []string {
|
|
t := start
|
|
|
|
// Save flags for performance.
|
|
hasYear := q.HasYear()
|
|
hasMonth := q.HasMonth()
|
|
hasDay := q.HasDay()
|
|
hasHour := q.HasHour()
|
|
|
|
var results []string
|
|
|
|
// Walk up from smallest units to largest units.
|
|
if hasHour || hasDay || hasMonth {
|
|
for t.Before(end) {
|
|
if hasHour {
|
|
if !nextDayGTE(t, end) {
|
|
break
|
|
} else if t.Hour() != 0 {
|
|
results = append(results, ViewByTimeUnit(name, t, 'H'))
|
|
t = t.Add(time.Hour)
|
|
continue
|
|
}
|
|
|
|
}
|
|
|
|
if hasDay {
|
|
if !nextMonthGTE(t, end) {
|
|
break
|
|
} else if t.Day() != 1 {
|
|
results = append(results, ViewByTimeUnit(name, t, 'D'))
|
|
t = t.AddDate(0, 0, 1)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if hasMonth {
|
|
if !nextYearGTE(t, end) {
|
|
break
|
|
} else if t.Month() != 1 {
|
|
results = append(results, ViewByTimeUnit(name, t, 'M'))
|
|
t = t.AddDate(0, 1, 0)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If a unit exists but isn't set and there are no larger units
|
|
// available then we need to exit the loop because we are no longer
|
|
// making progress.
|
|
break
|
|
}
|
|
}
|
|
|
|
// Walk back down from largest units to smallest units.
|
|
for t.Before(end) {
|
|
if hasYear && nextYearGTE(t, end) {
|
|
results = append(results, ViewByTimeUnit(name, t, 'Y'))
|
|
t = t.AddDate(1, 0, 0)
|
|
} else if hasMonth && nextMonthGTE(t, end) {
|
|
results = append(results, ViewByTimeUnit(name, t, 'M'))
|
|
t = t.AddDate(0, 1, 0)
|
|
} else if hasDay && nextDayGTE(t, end) {
|
|
results = append(results, ViewByTimeUnit(name, t, 'D'))
|
|
t = t.AddDate(0, 0, 1)
|
|
} else if hasHour {
|
|
results = append(results, ViewByTimeUnit(name, t, 'H'))
|
|
t = t.Add(time.Hour)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func nextYearGTE(t time.Time, end time.Time) bool {
|
|
next := t.AddDate(1, 0, 0)
|
|
if next.Year() == end.Year() {
|
|
return true
|
|
}
|
|
return end.After(next)
|
|
}
|
|
|
|
func nextMonthGTE(t time.Time, end time.Time) bool {
|
|
next := t.AddDate(0, 1, 0)
|
|
y1, m1, _ := next.Date()
|
|
y2, m2, _ := end.Date()
|
|
if (y1 == y2) && (m1 == m2) {
|
|
return true
|
|
}
|
|
return end.After(next)
|
|
}
|
|
|
|
func nextDayGTE(t time.Time, end time.Time) bool {
|
|
next := t.AddDate(0, 0, 1)
|
|
y1, m1, d1 := next.Date()
|
|
y2, m2, d2 := end.Date()
|
|
if (y1 == y2) && (m1 == m2) && (d1 == d2) {
|
|
return true
|
|
}
|
|
return end.After(next)
|
|
}
|