// Package `tracking` defines a generic track (sort of record) type // and a container type that allows storing of tracks in a SQL database. package tracking import ( sqllib "database/sql" "encoding/json" "fmt" "time" lib "git.sr.ht/~cco/go-scopes" "git.sr.ht/~cco/go-scopes/logging/log" "git.sr.ht/~cco/go-scopes/storage" sql "git.sr.ht/~cco/go-scopes/storage" ) type ItemFactory func(*Container, ...string) *Track // basic track implementation type Track struct { trackId lib.Ident Head lib.StrMap TimeStamp *time.Time Data lib.Map container *Container } func (tr *Track) TrackId() lib.Ident { return tr.trackId } func (tr *Track) Container() *Container { return tr.container } func MakeTrack(cont *Container, h ...string) *Track { tr := Track{ Head: lib.StrMap{}, container: cont, } tr.SetHead(h...) return &tr } func (tr *Track) AsSlice() []any { var sl []any for _, k := range tr.container.HeadFields { sl = append(sl, tr.Head[k]) } sl = append(sl, tr.TimeStamp, tr.Data) return sl } func (tr *Track) SetHead(h ...string) { for i, k := range tr.container.HeadFields { if i >= len(h) { break } if h[i] != "" { tr.Head[k] = h[i] } } } func (tr *Track) ScanP(rows *sql.Rows) error { tr.Head = lib.StrMap{} var d []any for range tr.container.HeadFields { var hv string d = append(d, &hv) } var ts, rd string d = append(d, &ts, &rd, &tr.trackId) err := rows.Scan(d...) for i, k := range tr.container.HeadFields { tr.Head[k] = *d[i].(*string) } tr.TimeStamp = ParseDateTime(ts) err = json.Unmarshal([]byte(rd), &tr.Data) if err != nil { log.Error(err).Msg("storage.tracking.ScanP") } return err } // basic container implementation type ContDef struct { ItemFactory ItemFactory TableName string HeadFields []string Indexes [][]string } type Container struct { *ContDef Storage *sql.Storage } func Tracks(db *sql.Storage) *Container { return &Container{container_definition, db} } type querySpec struct { Table string Headvals lib.StrSlice Scols lib.StrSlice Qucols, Ordcols lib.StrSlice Quspecs []struct{ Col, Op string } Ordspecs []ordSpec Limit int Quvals []any Cont *Container Params lib.StrMap } type ordSpec struct { Col string Desc bool } func (spec *querySpec) AddQu(col, op string) { spec.Quspecs = append(spec.Quspecs, struct{ Col, Op string }{col, op}) } func (spec *querySpec) AddOrd(col string, desc bool) { spec.Ordspecs = append(spec.Ordspecs, ordSpec{col, desc}) } func (spec *querySpec) setup(cont *Container) { if cont.Storage.Schema == "" { spec.Table = cont.TableName } else { spec.Table = fmt.Sprintf("%s.%s", cont.Storage.Schema, cont.TableName) } if spec.Scols == nil { spec.Scols = append(cont.HeadFields, "timestamp", "data", "trackid") } for i, v := range spec.Headvals { if v != "" { spec.Qucols = append(spec.Qucols, cont.HeadFields[i]) spec.Quvals = append(spec.Quvals, v) } } for _, c := range spec.Qucols { spec.AddQu(c, "=") } for _, c := range spec.Ordcols { spec.AddOrd(c, false) } spec.Cont = cont } func (cont *Container) Get(id lib.Ident) *Track { quSpec := &querySpec{ Qucols: lib.StrSlice{"trackid"}, Quvals: []any{id}, } return cont.QueryOne(quSpec) } func (cont *Container) QueryLast(hv ...string) *Track { quSpec := &querySpec{ Headvals: hv, Limit: 1, } quSpec.AddOrd("timestamp", true) return cont.QueryOne(quSpec) } func (cont *Container) QueryOne(quSpec *querySpec) *Track { var tr *Track proc := func(r *sql.Rows) error { tr = cont.ItemFactory(cont) return tr.ScanP(r) } quSpec.setup(cont) sql := storage.BuildSql(SqlSelect, quSpec) cont.Storage.Query(proc, sql, quSpec.Quvals...) return tr } func (cont *Container) Query(quSpec *querySpec) []*Track { var trs []*Track proc := func(r *sql.Rows) error { tr := cont.ItemFactory(cont) err := tr.ScanP(r) if err == nil { trs = append(trs, tr) } return err } quSpec.setup(cont) sql := storage.BuildSql(SqlSelect, quSpec) cont.Storage.Query(proc, sql, quSpec.Quvals...) return trs } func (cont *Container) NewTrack(h []string, data lib.Map) *Track { tr := cont.ItemFactory(cont, h...) tr.Data = data cont.Insert(tr) return tr } func (cont *Container) Save(tr *Track) *Track { return tr } func (cont *Container) Insert(tr *Track) *Track { quSpec := &querySpec{ Scols: append(cont.HeadFields, "Data"), } quSpec.setup(cont) sql := storage.BuildSql(SqlInsert, quSpec) var values []any for _, k := range cont.HeadFields { values = append(values, tr.Head[k]) } b, _ := json.Marshal(tr.Data) values = append(values, b) var tsstr string proc := func(r *sqllib.Rows) error { err := r.Scan(&tr.trackId, &tsstr) tr.TimeStamp = ParseDateTime(tsstr) return err } if err := cont.Storage.Query(proc, sql, values...); err == nil { return tr } return nil } func (cont *Container) Update(tr *Track) *Track { quSpec := &querySpec{ Qucols: lib.StrSlice{"trackid"}, } quSpec.setup(cont) sql := storage.BuildSql(SqlUpdate, quSpec) n, _ := cont.Storage.Exec(sql, append(tr.AsSlice(), tr.trackId)...) if n == 1 { return tr } return nil } func ParseDateTime(inp string) *time.Time { ts, err := time.Parse("2006-01-02T15:04:05-07:00", inp) if err == nil { return &ts } ts, err = time.Parse("2006-01-02 15:04:05", inp) if err == nil { return &ts } log.Error(err).Msg("storage.tracking.ParseDateTime") return nil } func (cont *Container) CreateTable() { spec := querySpec{} spec.setup(cont) sql := storage.BuildSql(SqlCreate, spec) if _, err := cont.Storage.Exec(sql); err != nil { panic(err) } } // container definition var container_definition *ContDef func init() { container_definition = &ContDef{ ItemFactory: MakeTrack, TableName: "tracks", HeadFields: []string{"taskId", "userName"}, Indexes: [][]string{ []string{"taskId", "userName"}, []string{"userName"}, }, } }