68347e8b95
abandon generic query frag for common structures Instead of using the QueryFrag struct, which was too generic to be generally useful, we have moved to a BusEventFilter type, which contains things we may filter on when we're searching for bus events. At the moment it just contains names, and start/stop times. Then in each function we can accept this filter struct and convert it to fit the query. We also support general modifiers, and currently have one implemented: the LimitOffsetModifier. This adds a LIMIT and OFFSET clause to any statement. these are all applied at the end and receive a stringbuilder which may prevent certain operations from being structured. We need to work on this one more, potentially abandoning.
143 lines
3.6 KiB
Go
143 lines
3.6 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kschamplin/gotelem/skylab"
|
|
)
|
|
|
|
// Modifier augments SQL strings.
|
|
type Modifier interface {
|
|
ModifyStatement(*strings.Builder) error
|
|
}
|
|
|
|
// LimitOffsetModifier is a modifier to support pagniation.
|
|
type LimitOffsetModifier struct {
|
|
Limit int
|
|
Offset int
|
|
}
|
|
|
|
func (l LimitOffsetModifier) ModifyStatement(sb *strings.Builder) error {
|
|
clause := fmt.Sprintf(" LIMIT %d OFFSET %d", l.Limit, l.Offset)
|
|
sb.WriteString(clause)
|
|
return nil
|
|
}
|
|
|
|
// BusEventFilter is a filter for bus events.
|
|
type BusEventFilter struct {
|
|
Names []string
|
|
TimerangeStart time.Time
|
|
TimerangeEnd time.Time
|
|
}
|
|
|
|
// now we can optionally add a limit.
|
|
|
|
func (tdb *TelemDb) GetPackets(ctx context.Context, filter BusEventFilter, options ...Modifier) ([]skylab.BusEvent, error) {
|
|
// construct a simple
|
|
var whereFrags = make([]string, 0)
|
|
|
|
// if we're filtering by names, add a where clause for it.
|
|
if len(filter.Names) > 0 {
|
|
names := strings.Join(filter.Names, ", ")
|
|
qString := fmt.Sprintf("name IN (%s)", names)
|
|
whereFrags = append(whereFrags, qString)
|
|
}
|
|
// TODO: identify if we need a special case for both time ranges
|
|
// using BETWEEN since apparenlty that can be better?
|
|
|
|
// next, check if we have a start/end time, add constraints
|
|
if !filter.TimerangeEnd.IsZero() {
|
|
qString := fmt.Sprintf("ts <= %d", filter.TimerangeEnd.UnixMilli())
|
|
whereFrags = append(whereFrags, qString)
|
|
}
|
|
if !filter.TimerangeStart.IsZero() {
|
|
// we have an end range
|
|
qString := fmt.Sprintf("ts >= %d", filter.TimerangeStart.UnixMilli())
|
|
whereFrags = append(whereFrags, qString)
|
|
}
|
|
|
|
sb := strings.Builder{}
|
|
sb.WriteString("SELECT * from \"bus_events\"")
|
|
// construct the full statement.
|
|
if len(whereFrags) > 0 {
|
|
// use the where clauses.
|
|
sb.WriteString(" WHERE ")
|
|
sb.WriteString(strings.Join(whereFrags, " AND "))
|
|
}
|
|
|
|
// Augment our data further if there's i.e a limit modifier.
|
|
// TODO: factor this out maybe?
|
|
for _, m := range options {
|
|
m.ModifyStatement(&sb)
|
|
}
|
|
rows, err := tdb.db.QueryxContext(ctx, sb.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var events = make([]skylab.BusEvent, 0, 10)
|
|
|
|
for rows.Next() {
|
|
var ev skylab.RawJsonEvent
|
|
err := rows.Scan(&ev.Timestamp, &ev.Name, (*[]byte)(&ev.Data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
BusEv := skylab.BusEvent {
|
|
Timestamp: time.UnixMilli(int64(ev.Timestamp)),
|
|
Name: ev.Name,
|
|
}
|
|
BusEv.Data, err = skylab.FromJson(ev.Name, ev.Data)
|
|
events = append(events, BusEv)
|
|
}
|
|
|
|
err = rows.Err()
|
|
|
|
return events, err
|
|
}
|
|
|
|
// Datum is a single measurement - it is more granular than a packet.
|
|
// the classic example is bms_measurement.current
|
|
type Datum struct {
|
|
Timestamp time.Time `db:"timestamp"`
|
|
Value any `db:"val"`
|
|
}
|
|
|
|
// GetValues queries the database for values in a given time range.
|
|
// A value is a specific data point. For example, bms_measurement.current
|
|
// would be a value.
|
|
func (tdb *TelemDb) GetValues(ctx context.Context, packetName, field string, start time.Time,
|
|
end time.Time) ([]Datum, error) {
|
|
// this fragment uses json_extract from sqlite to get a single
|
|
// nested value.
|
|
SqlFrag := `
|
|
SELECT
|
|
ts as timestamp,
|
|
json_extract(data, '$.' || ?) as val
|
|
FROM bus_events WHERE name IS ?
|
|
`
|
|
rows, err := tdb.db.QueryxContext(ctx, SqlFrag, field, packetName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
data := make([]Datum, 0, 10)
|
|
for rows.Next() {
|
|
var d Datum = Datum{}
|
|
err = rows.StructScan(&d)
|
|
if err != nil {
|
|
fmt.Print(err)
|
|
return data, err
|
|
}
|
|
data = append(data, d)
|
|
}
|
|
fmt.Print(rows.Err())
|
|
|
|
return data, nil
|
|
}
|