package main import ( // External werr "git.gibonuddevalla.se/go/wrappederror" "github.com/jmoiron/sqlx" // Standard "database/sql" "errors" "strings" "time" ) type DatapointType string const ( INT DatapointType = "INT" STRING = "STRING" DATETIME = "DATETIME" ) type Datapoint struct { ID int Name string Datatype DatapointType LastValue time.Time `db:"last_value"` DatapointValueJSON []byte `db:"datapoint_value_json"` LastDatapointValue DatapointValue Found bool } type DatapointValue struct { ID int DatapointID int `db:"datapoint_id"` Ts time.Time ValueInt sql.NullInt64 `db:"value_int"` ValueString sql.NullString `db:"value_string"` ValueDateTime sql.NullTime `db:"value_datetime"` TemplateValue any } func (dp DatapointValue) Value() any { // {{{ if dp.ValueInt.Valid { return dp.ValueInt.Int64 } if dp.ValueString.Valid { return dp.ValueString.String } if dp.ValueDateTime.Valid { return dp.ValueDateTime.Time } return nil } // }}} func (dp DatapointValue) FormattedTime() string {// {{{ if dp.ValueDateTime.Valid { return dp.ValueDateTime.Time.Format("2006-01-02 15:04:05") } return "invalid time" }// }}} func (dp Datapoint) Update() (err error) {// {{{ name := strings.TrimSpace(dp.Name) if name == "" { err = errors.New("Name can't be empty") return } if dp.ID == 0 { _, err = service.Db.Conn.Exec( `INSERT INTO datapoint(name, datatype) VALUES($1, $2)`, name, dp.Datatype, ) } else { _, err = service.Db.Conn.Exec( `UPDATE datapoint SET name=$2, datatype=$3 WHERE id=$1`, dp.ID, name, dp.Datatype, ) } return }// }}} func DatapointAdd[T any](name string, value T) (err error) { // {{{ row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name) var dpID int var dpType DatapointType err = row.Scan(&dpID, &dpType) if err != nil { err = werr.Wrap(err).WithData(struct { Name string Value any }{name, value}) return } switch dpType { case INT: _, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_int) VALUES($1, $2)`, dpID, value) case STRING: _, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_string) VALUES($1, $2)`, dpID, value) case DATETIME: _, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_datetime) VALUES($1, $2)`, dpID, value) } if err != nil { err = werr.Wrap(err).WithData(struct { ID int value any }{dpID, value}) return } return } // }}} func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{ dps = []Datapoint{} var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` SELECT dp.id, dp.name, dp.datatype, dp.last_value, dpv.id AS v_id, dpv.ts, dpv.value_int, dpv.value_string, dpv.value_datetime FROM public.datapoint dp LEFT JOIN ( SELECT *, row_number() OVER (PARTITION BY "datapoint_id" ORDER BY ts DESC) AS rn FROM datapoint_value ) dpv ON dpv.datapoint_id = dp.id AND rn = 1 ORDER BY dp.name ASC `) if err != nil { err = werr.Wrap(err) } defer rows.Close() type DbRes struct { ID int Name string Datatype DatapointType LastValue time.Time `db:"last_value"` VID sql.NullInt64 `db:"v_id"` Ts sql.NullTime ValueInt sql.NullInt64 `db:"value_int"` ValueString sql.NullString `db:"value_string"` ValueDateTime sql.NullTime `db:"value_datetime"` } for rows.Next() { dp := Datapoint{} dpv := DatapointValue{} res := DbRes{} err = rows.StructScan(&res) if err != nil { err = werr.Wrap(err) return } dp.ID = res.ID dp.Name = res.Name dp.Datatype = res.Datatype dp.LastValue = res.LastValue dp.Found = true if res.VID.Valid { dpv.ID = int(res.VID.Int64) dpv.Ts = res.Ts.Time dpv.ValueInt = res.ValueInt dpv.ValueString = res.ValueString dpv.ValueDateTime = res.ValueDateTime dp.LastDatapointValue = dpv } dps = append(dps, dp) } return } // }}} func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ var query string var param any if id > 0 { query = `SELECT *, true AS found FROM datapoint WHERE id = $1` param = id dp.ID = id } else { query = `SELECT *, true AS found FROM datapoint WHERE name = $1` param = name } row := service.Db.Conn.QueryRowx(query, param) err = row.StructScan(&dp) if err == sql.ErrNoRows { dp = Datapoint{ Name: name, } err = nil return } if err != nil { err = werr.Wrap(err).WithData(name) return } row = service.Db.Conn.QueryRowx(` SELECT * FROM datapoint_value WHERE datapoint_id = $1 ORDER BY ts DESC LIMIT 1 `, dp.ID, ) err = row.StructScan(&dp.LastDatapointValue) if err == sql.ErrNoRows { err = nil return } if err != nil { err = werr.Wrap(err).WithData(dp.ID) return } return } // }}} func DatapointDelete(id int) (err error) {// {{{ _, err = service.Db.Conn.Exec(`DELETE FROM datapoint WHERE id=$1`, id) if err != nil { err = werr.Wrap(err).WithData(id) } return }// }}} func DatapointValues(id int) (values []DatapointValue, err error) {// {{{ rows, err := service.Db.Conn.Queryx(`SELECT * FROM datapoint_value WHERE datapoint_id=$1 ORDER BY ts DESC LIMIT 500`, id) if err != nil { err = werr.Wrap(err).WithData(id) return } defer rows.Close() for rows.Next() { dpv := DatapointValue{} err = rows.StructScan(&dpv) if err != nil { err = werr.Wrap(err).WithData(id) return } values = append(values, dpv) } return }// }}}