New DB schema

This commit is contained in:
Daniel Ponte 2024-11-15 10:37:58 -05:00
parent 05eccf588b
commit af80e46068
16 changed files with 111 additions and 352 deletions

View file

@ -228,11 +228,11 @@ func (as *alerter) scoredTGs() []talkgroups.ID {
return tgs return tgs
} }
// packedScoredTGs gets a list of packed TGIDs. // packedScoredTGs gets a list of TGID tuples.
func (as *alerter) scoredTGsTuple() []database.TalkgroupT { func (as *alerter) scoredTGsTuple() (tgs database.TGTuples) {
tgs := make([]database.TalkgroupT, 0, len(as.scores)) tgs = database.MakeTGTuples(len(as.scores))
for _, s := range as.scores { for _, s := range as.scores {
tgs = append(tgs, s.ID.Tuple()) tgs.Append(s.ID.System, s.ID.Talkgroup)
} }
return tgs return tgs

View file

@ -40,7 +40,7 @@ func (as *alerter) tgStatsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
db := database.FromCtx(ctx) db := database.FromCtx(ctx)
tgs, err := db.GetTalkgroupsWithLearnedByPackedIDs(ctx, as.scoredTGsTuple()) tgs, err := db.GetTalkgroupsWithLearnedBySysTGID(ctx, as.scoredTGsTuple())
if err != nil { if err != nil {
log.Error().Err(err).Msg("stats TG get failed") log.Error().Err(err).Msg("stats TG get failed")
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View file

@ -38,6 +38,7 @@ type CORS struct {
type DB struct { type DB struct {
Connect string `yaml:"connect"` Connect string `yaml:"connect"`
LogQueries bool `yaml:"logQueries"`
} }
type Logger struct { type Logger struct {

View file

@ -21,9 +21,9 @@ type DB struct {
*Queries *Queries
} }
type myLogger struct{} type dbLogger struct{}
func (m myLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) { func (m dbLogger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) {
log.Debug().Fields(data).Msg(msg) log.Debug().Fields(data).Msg(msg)
} }
@ -51,12 +51,13 @@ func NewClient(ctx context.Context, conf config.DB) (*DB, error) {
return nil, err return nil, err
} }
logger := myLogger{} if conf.LogQueries {
tracer := &tracelog.TraceLog{ pgConf.ConnConfig.Tracer = &tracelog.TraceLog{
Logger: logger, Logger: dbLogger{},
LogLevel: tracelog.LogLevelTrace, LogLevel: tracelog.LogLevelTrace,
} }
pgConf.ConnConfig.Tracer = tracer }
pool, err := pgxpool.NewWithConfig(ctx, pgConf) pool, err := pgxpool.NewWithConfig(ctx, pgConf)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -3,6 +3,9 @@ package database
func (d GetTalkgroupsRow) GetTalkgroup() Talkgroup { return d.Talkgroup } func (d GetTalkgroupsRow) GetTalkgroup() Talkgroup { return d.Talkgroup }
func (d GetTalkgroupsRow) GetSystem() System { return d.System } func (d GetTalkgroupsRow) GetSystem() System { return d.System }
func (d GetTalkgroupsRow) GetLearned() bool { return d.Learned } func (d GetTalkgroupsRow) GetLearned() bool { return d.Learned }
func (g GetTalkgroupWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g GetTalkgroupWithLearnedRow) GetSystem() System { return g.System }
func (g GetTalkgroupWithLearnedRow) GetLearned() bool { return g.Learned }
func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup } func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System } func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System }
func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Learned } func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Learned }

View file

@ -14,7 +14,6 @@ import (
type Querier interface { type Querier interface {
AddAlert(ctx context.Context, arg AddAlertParams) error AddAlert(ctx context.Context, arg AddAlertParams) error
AddCall(ctx context.Context, arg AddCallParams) error AddCall(ctx context.Context, arg AddCallParams) error
BulkSetTalkgroupTags(ctx context.Context, iD uuid.UUID, tags []string) error
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteAPIKey(ctx context.Context, apiKey string) error DeleteAPIKey(ctx context.Context, apiKey string) error
@ -22,9 +21,9 @@ type Querier interface {
GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error) GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error)
GetDatabaseSize(ctx context.Context) (string, error) GetDatabaseSize(ctx context.Context) (string, error)
GetSystemName(ctx context.Context, systemID int) (string, error) GetSystemName(ctx context.Context, systemID int) (string, error)
GetTalkgroup(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupRow, error) GetTalkgroup(ctx context.Context, systemID int32, tgID int32) (GetTalkgroupRow, error)
GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, alltags []string, nottags []string) ([]GetTalkgroupIDsByTagsRow, error)
GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) GetTalkgroupTags(ctx context.Context, systemID int32, tgID int32) ([]string, error)
GetTalkgroupWithLearned(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupWithLearnedRow, error) GetTalkgroupWithLearned(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupWithLearnedRow, error)
GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAllTagsRow, error) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAllTagsRow, error)
GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error)
@ -35,7 +34,7 @@ type Querier interface {
GetUserByUsername(ctx context.Context, username string) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error)
GetUsers(ctx context.Context) ([]User, error) GetUsers(ctx context.Context) ([]User, error)
SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error
SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []string) error SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tgID int32) error
UpdatePassword(ctx context.Context, username string, password string) error UpdatePassword(ctx context.Context, username string, password string) error
UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error)
} }

View file

@ -2,41 +2,25 @@ package database
import ( import (
"context" "context"
"database/sql/driver"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
) )
type TalkgroupT struct { type TGTuples [2][]uint32
System uint32 `json:"system_id"`
Talkgroup uint32 `json:"tgid"`
}
type TalkgroupTs []TalkgroupT func MakeTGTuples(cap int) TGTuples {
return [2][]uint32{
func (t TalkgroupTs) Nest() (sys []uint32, tg []uint32) { make([]uint32, 0, cap),
sys = make([]uint32, len(t)) make([]uint32, 0, cap),
tg = make([]uint32, len(t))
for i := range t {
sys[i] = t[i].System
tg[i] = t[i].Talkgroup
} }
return
} }
func (t TalkgroupT) Value() (driver.Value, error) { func (t *TGTuples) Append(sys, tg uint32) {
return [2]uint32{t.System, t.Talkgroup}, nil t[0] = append(t[0], sys)
t[1] = append(t[1], tg)
} }
func (t TalkgroupT) TextValue() (pgtype.Text, error) { // Below queries are here because sqlc refuses to parse unnest(x, y)
return pgtype.Text{String: fmt.Sprintf("%d:%d", t.System, t.Talkgroup)}, nil
}
const getTalkgroupsWithLearnedByPackedIDs = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many const getTalkgroupsWithLearnedBySysTGID = `SELECT
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned FALSE learned
FROM talkgroups tg FROM talkgroups tg
@ -59,9 +43,8 @@ type GetTalkgroupsRow struct {
Learned bool `json:"learned"` Learned bool `json:"learned"`
} }
func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids TalkgroupTs) ([]GetTalkgroupsRow, error) { func (q *Queries) GetTalkgroupsWithLearnedBySysTGID(ctx context.Context, ids TGTuples) ([]GetTalkgroupsRow, error) {
sysAr, tgAr := ids.Nest() rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedBySysTGID, ids[0], ids[1])
rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedByPackedIDs, sysAr, tgAr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -96,15 +79,12 @@ func (q *Queries) GetTalkgroupsWithLearnedByPackedIDs(ctx context.Context, ids T
return items, nil return items, nil
} }
const getTalkgroupsByPackedIDs = `-- name: GetTalkgroupsByPackedIDs :many const getTalkgroupsBySysTGID = `SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg
SELECT tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id JOIN systems sys ON tg.system_id = sys.id
JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tg.system_id = tgt.sys AND tg.tgid = tgt.tg) JOIN UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) ON (tg.system_id = tgt.sys AND tg.tgid = tgt.tg);`
`
func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, ids TalkgroupTs) ([]GetTalkgroupsRow, error) { func (q *Queries) GetTalkgroupsBySysTGID(ctx context.Context, ids TGTuples) ([]GetTalkgroupsRow, error) {
sysAr, tgAr := ids.Nest() rows, err := q.db.Query(ctx, getTalkgroupsBySysTGID, ids[0], ids[1])
rows, err := q.db.Query(ctx, getTalkgroupsByPackedIDs, sysAr, tgAr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -137,3 +117,10 @@ func (q *Queries) GetTalkgroupsByPackedIDs(ctx context.Context, ids TalkgroupTs)
} }
return items, nil return items, nil
} }
const bulkSetTalkgroupTags = `UPDATE talkgroups tg SET tags = $3 FROM UNNEST($1::INT4[], $2::INT4[]) AS tgt(sys, tg) WHERE (tg.system_id = tgt.sys AND tg.tgid = tgt.tg);`
func (q *Queries) BulkSetTalkgroupTags(ctx context.Context, tgs TGTuples, tags []string) error {
_, err := q.db.Exec(ctx, bulkSetTalkgroupTags, tgs[0], tgs[1], tags)
return err
}

View file

@ -10,20 +10,9 @@ import (
"dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/alerting/rules" "dynatron.me/x/stillbox/pkg/alerting/rules"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const bulkSetTalkgroupTags = `-- name: BulkSetTalkgroupTags :exec
UPDATE talkgroups SET tags = $2
WHERE id = ANY($1)
`
func (q *Queries) BulkSetTalkgroupTags(ctx context.Context, iD uuid.UUID, tags []string) error {
_, err := q.db.Exec(ctx, bulkSetTalkgroupTags, iD, tags)
return err
}
const getSystemName = `-- name: GetSystemName :one const getSystemName = `-- name: GetSystemName :one
SELECT name FROM systems WHERE id = $1 SELECT name FROM systems WHERE id = $1
` `
@ -44,8 +33,8 @@ type GetTalkgroupRow struct {
Talkgroup Talkgroup `json:"talkgroup"` Talkgroup Talkgroup `json:"talkgroup"`
} }
func (q *Queries) GetTalkgroup(ctx context.Context, systemID int32, tgid int32) (GetTalkgroupRow, error) { func (q *Queries) GetTalkgroup(ctx context.Context, systemID int32, tgID int32) (GetTalkgroupRow, error) {
row := q.db.QueryRow(ctx, getTalkgroup, systemID, tgid) row := q.db.QueryRow(ctx, getTalkgroup, systemID, tgID)
var i GetTalkgroupRow var i GetTalkgroupRow
err := row.Scan( err := row.Scan(
&i.Talkgroup.ID, &i.Talkgroup.ID,
@ -98,11 +87,11 @@ func (q *Queries) GetTalkgroupIDsByTags(ctx context.Context, anytags []string, a
const getTalkgroupTags = `-- name: GetTalkgroupTags :one const getTalkgroupTags = `-- name: GetTalkgroupTags :one
SELECT tags FROM talkgroups SELECT tags FROM talkgroups
WHERE id = systg2id($1, $2) WHERE system_id = $1 AND tgid = $2
` `
func (q *Queries) GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) { func (q *Queries) GetTalkgroupTags(ctx context.Context, systemID int32, tgID int32) ([]string, error) {
row := q.db.QueryRow(ctx, getTalkgroupTags, sys, tg) row := q.db.QueryRow(ctx, getTalkgroupTags, systemID, tgID)
var tags []string var tags []string
err := row.Scan(&tags) err := row.Scan(&tags)
return tags, err return tags, err
@ -117,7 +106,7 @@ JOIN systems sys ON tg.system_id = sys.id
WHERE (tg.system_id, tg.tgid) = ($1, $2) WHERE (tg.system_id, tg.tgid) = ($1, $2)
UNION UNION
SELECT SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name, TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
@ -362,12 +351,12 @@ func (q *Queries) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system i
} }
const setTalkgroupTags = `-- name: SetTalkgroupTags :exec const setTalkgroupTags = `-- name: SetTalkgroupTags :exec
UPDATE talkgroups SET tags = $3 UPDATE talkgroups SET tags = $1
WHERE id = systg2id($1, $2) WHERE system_id = $2 AND tgid = $3
` `
func (q *Queries) SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []string) error { func (q *Queries) SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tgID int32) error {
_, err := q.db.Exec(ctx, setTalkgroupTags, sys, tg, tags) _, err := q.db.Exec(ctx, setTalkgroupTags, tags, systemID, tgID)
return err return err
} }

View file

@ -6,34 +6,16 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const getTalkgroupsWithLearnedByPackedIDsTest = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.id = ANY($1::INT8[])
UNION
SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned
FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id
WHERE systg2id(tgl.system_id, tgl.tgid) = ANY($1::INT8[]) AND ignored IS NOT TRUE
`
const getTalkgroupWithLearnedTest = `-- name: GetTalkgroupWithLearned :one const getTalkgroupWithLearnedTest = `-- name: GetTalkgroupWithLearned :one
SELECT SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name, tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
FALSE learned FALSE learned
FROM talkgroups tg FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id JOIN systems sys ON tg.system_id = sys.id
WHERE tg.id = systg2id($1, $2) WHERE (tg.system_id, tg.tgid) = ($1, $2)
UNION UNION
SELECT SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name, TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
@ -52,7 +34,7 @@ JOIN systems sys ON tg.system_id = sys.id
WHERE tg.system_id = $1 WHERE tg.system_id = $1
UNION UNION
SELECT SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name, TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
@ -70,7 +52,7 @@ FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id JOIN systems sys ON tg.system_id = sys.id
UNION UNION
SELECT SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name, TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
@ -81,7 +63,6 @@ WHERE ignored IS NOT TRUE
` `
func TestQueryColumnsMatch(t *testing.T) { func TestQueryColumnsMatch(t *testing.T) {
require.Equal(t, getTalkgroupsWithLearnedByPackedIDsTest, getTalkgroupsWithLearnedByPackedIDs)
require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned) require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned)
require.Equal(t, getTalkgroupsWithLearnedBySystemTest, getTalkgroupsWithLearnedBySystem) require.Equal(t, getTalkgroupsWithLearnedBySystemTest, getTalkgroupsWithLearnedBySystem)
require.Equal(t, getTalkgroupsWithLearnedTest, getTalkgroupsWithLearned) require.Equal(t, getTalkgroupsWithLearnedTest, getTalkgroupsWithLearned)

View file

@ -39,8 +39,8 @@ type Store interface {
// Hint hints the Store that the provided talkgroups will be asked for. // Hint hints the Store that the provided talkgroups will be asked for.
Hint(ctx context.Context, tgs []ID) error Hint(ctx context.Context, tgs []ID) error
// Load loads the provided packed talkgroup IDs into the Store. // Load loads the provided talkgroup ID tuples into the Store.
Load(ctx context.Context, tgs []database.TalkgroupT) error Load(ctx context.Context, tgs database.TGTuples) error
// Invalidate invalidates any caching in the Store. // Invalidate invalidates any caching in the Store.
Invalidate() Invalidate()
@ -98,19 +98,20 @@ func NewCache() Store {
func (t *cache) Hint(ctx context.Context, tgs []ID) error { func (t *cache) Hint(ctx context.Context, tgs []ID) error {
t.RLock() t.RLock()
var toLoad []database.TalkgroupT var toLoad database.TGTuples
if len(t.tgs) > len(tgs)/2 { // TODO: instrument this if len(t.tgs) > len(tgs)/2 { // TODO: instrument this
for _, tg := range tgs { for _, tg := range tgs {
_, ok := t.tgs[tg] _, ok := t.tgs[tg]
if !ok { if !ok {
toLoad = append(toLoad, tg.Tuple()) toLoad.Append(tg.System, tg.Talkgroup)
} }
} }
} else { } else {
toLoad = make([]database.TalkgroupT, 0, len(tgs)) toLoad[0] = make([]uint32, 0, len(tgs))
toLoad[1] = make([]uint32, 0, len(tgs))
for _, g := range tgs { for _, g := range tgs {
toLoad = append(toLoad, g.Tuple()) toLoad.Append(g.System, g.Talkgroup)
} }
} }
@ -136,7 +137,7 @@ func (t *cache) add(rec *Talkgroup) error {
type row interface { type row interface {
database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow | database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow |
database.GetTalkgroupsWithLearnedBySystemRow database.GetTalkgroupsWithLearnedBySystemRow | database.GetTalkgroupWithLearnedRow
GetTalkgroup() database.Talkgroup GetTalkgroup() database.Talkgroup
GetSystem() database.System GetSystem() database.System
GetLearned() bool GetLearned() bool
@ -180,7 +181,7 @@ func (t *cache) TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error) {
} }
t.RUnlock() t.RUnlock()
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, toGet.Tuples()) tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, toGet.Tuples())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -196,8 +197,8 @@ func (t *cache) TGs(ctx context.Context, tgs IDs) ([]*Talkgroup, error) {
return addToRowList(t, r, tgRecords) return addToRowList(t, r, tgRecords)
} }
func (t *cache) Load(ctx context.Context, tgs []database.TalkgroupT) error { func (t *cache) Load(ctx context.Context, tgs database.TGTuples) error {
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, tgs) tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, tgs)
if err != nil { if err != nil {
return err return err
} }
@ -245,7 +246,7 @@ func (t *cache) TG(ctx context.Context, tg ID) (*Talkgroup, error) {
return rec, nil return rec, nil
} }
recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedByPackedIDs(ctx, []database.TalkgroupT{tg.Tuple()}) record, err := database.FromCtx(ctx).GetTalkgroupWithLearned(ctx, int32(tg.System), int32(tg.Talkgroup))
switch err { switch err {
case nil: case nil:
case pgx.ErrNoRows: case pgx.ErrNoRows:
@ -255,17 +256,13 @@ func (t *cache) TG(ctx context.Context, tg ID) (*Talkgroup, error) {
return nil, errors.Join(ErrNotFound, err) return nil, errors.Join(ErrNotFound, err)
} }
if len(recs) < 1 { err = t.add(rowToTalkgroup(record))
return nil, ErrNotFound
}
err = t.add(rowToTalkgroup(recs[0]))
if err != nil { if err != nil {
log.Error().Err(err).Msg("TG() cache add") log.Error().Err(err).Msg("TG() cache add")
return rowToTalkgroup(recs[0]), errors.Join(ErrNotFound, err) return rowToTalkgroup(record), errors.Join(ErrNotFound, err)
} }
return rowToTalkgroup(recs[0]), nil return rowToTalkgroup(record), nil
} }
func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool) { func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool) {

View file

@ -26,13 +26,16 @@ type ID struct {
type IDs []ID type IDs []ID
func (ids *IDs) Tuples() []database.TalkgroupT { func (t IDs) Tuples() database.TGTuples {
r := make([]database.TalkgroupT, len(*ids)) sys := make([]uint32, len(t))
for i := range *ids { tg := make([]uint32, len(t))
r[i] = (*ids)[i].Tuple()
for i := range t {
sys[i] = t[i].System
tg[i] = t[i].Talkgroup
} }
return r return database.TGTuples{sys, tg}
} }
type intId interface { type intId interface {
@ -46,13 +49,6 @@ func TG[T intId, U intId](sys T, tgid U) ID {
} }
} }
func (t ID) Tuple() database.TalkgroupT {
return database.TalkgroupT{
System: t.System,
Talkgroup: t.Talkgroup,
}
}
func (t ID) String() string { func (t ID) String() string {
return fmt.Sprintf("%d:%d", t.System, t.Talkgroup) return fmt.Sprintf("%d:%d", t.System, t.Talkgroup)

View file

@ -23,31 +23,10 @@ CREATE TABLE IF NOT EXISTS systems(
name TEXT NOT NULL name TEXT NOT NULL
); );
CREATE OR REPLACE FUNCTION systg2id(_sys INTEGER, _tg INTEGER) RETURNS INT8 LANGUAGE plpgsql AS
$$
BEGIN
RETURN ((_sys::BIGINT << 32) | _tg);
END
$$;
CREATE OR REPLACE FUNCTION tgfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS
$$
BEGIN
RETURN (_id & x'ffffffff'::BIGINT);
END
$$;
CREATE OR REPLACE FUNCTION sysfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS
$$
BEGIN
RETURN (_id >> 32);
END
$$;
CREATE TABLE IF NOT EXISTS talkgroups( CREATE TABLE IF NOT EXISTS talkgroups(
id INT8 PRIMARY KEY, id UUID PRIMARY KEY,
system_id INT4 REFERENCES systems(id) NOT NULL GENERATED ALWAYS AS (id >> 32) STORED, system_id INT4 REFERENCES systems(id) NOT NULL,
tgid INT4 NOT NULL GENERATED ALWAYS AS (id & x'ffffffff'::BIGINT) STORED, tgid INT4 NOT NULL,
name TEXT, name TEXT,
alpha_tag TEXT, alpha_tag TEXT,
tg_group TEXT, tg_group TEXT,
@ -56,9 +35,12 @@ CREATE TABLE IF NOT EXISTS talkgroups(
tags TEXT[] NOT NULL DEFAULT '{}', tags TEXT[] NOT NULL DEFAULT '{}',
alert BOOLEAN NOT NULL DEFAULT 'true', alert BOOLEAN NOT NULL DEFAULT 'true',
alert_config JSONB, alert_config JSONB,
weight REAL NOT NULL DEFAULT 1.0 weight REAL NOT NULL DEFAULT 1.0,
UNIQUE (system_id, tgid)
); );
CREATE INDEX talkgroups_system_tgid_idx ON talkgroups (system_id, tgid);
CREATE INDEX IF NOT EXISTS talkgroup_id_tags ON talkgroups USING GIN (tags); CREATE INDEX IF NOT EXISTS talkgroup_id_tags ON talkgroups USING GIN (tags);
CREATE TABLE IF NOT EXISTS talkgroups_learned( CREATE TABLE IF NOT EXISTS talkgroups_learned(

View file

@ -1,9 +0,0 @@
DROP INDEX IF EXISTS talkgroups_system_tgid_idx;
ALTER TABLE talkgroups ALTER COLUMN id SET DATA TYPE INT8 USING (systg2id(system_id, tgid));
ALTER TABLE talkgroups DROP COLUMN IF EXISTS tgid;
ALTER TABLE talkgroups ADD COLUMN IF NOT EXISTS tgid INT4 NOT NULL GENERATED ALWAYS AS (id & x'ffffffff'::BIGINT) STORED,
ALTER TABLE talkgroups DROP COLUMN IF EXISTS system_id;
ALTER TABLE talkgroups ADD COLUMN IF NOT EXISTS system_id INT4 REFERENCES systems(id) NOT NULL GENERATED ALWAYS AS (id >> 32) STORED;

View file

@ -1,7 +0,0 @@
ALTER TABLE talkgroups ALTER COLUMN system_id DROP EXPRESSION;
ALTER TABLE talkgroups ALTER COLUMN tgid DROP EXPRESSION;
ALTER TABLE talkgroups ALTER COLUMN id SET DATA TYPE UUID USING (gen_random_uuid());
CREATE INDEX IF NOT EXISTS talkgroups_system_tgid_idx ON talkgroups (system_id, tgid);

View file

@ -1,157 +0,0 @@
CREATE TABLE IF NOT EXISTS users(
id SERIAL PRIMARY KEY,
username VARCHAR (255) UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT NOT NULL,
is_admin BOOLEAN NOT NULL,
prefs JSONB
);
CREATE INDEX IF NOT EXISTS users_username_idx ON users(username);
CREATE TABLE IF NOT EXISTS api_keys(
id SERIAL PRIMARY KEY,
owner INTEGER REFERENCES users(id) NOT NULL,
created_at TIMESTAMP NOT NULL,
expires TIMESTAMP,
disabled BOOLEAN,
api_key TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS systems(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE OR REPLACE FUNCTION systg2id(_sys INTEGER, _tg INTEGER) RETURNS INT8 LANGUAGE plpgsql AS
$$
BEGIN
RETURN ((_sys::BIGINT << 32) | _tg);
END
$$;
CREATE OR REPLACE FUNCTION tgfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS
$$
BEGIN
RETURN (_id & x'ffffffff'::BIGINT);
END
$$;
CREATE OR REPLACE FUNCTION sysfromid(_id INT8) RETURNS INTEGER LANGUAGE plpgsql AS
$$
BEGIN
RETURN (_id >> 32);
END
$$;
CREATE TABLE IF NOT EXISTS talkgroups(
id UUID PRIMARY KEY,
system_id INT4 REFERENCES systems(id) NOT NULL,
tgid INT4 NOT NULL,
name TEXT,
alpha_tag TEXT,
tg_group TEXT,
frequency INTEGER,
metadata JSONB,
tags TEXT[] NOT NULL DEFAULT '{}',
alert BOOLEAN NOT NULL DEFAULT 'true',
alert_config JSONB,
weight REAL NOT NULL DEFAULT 1.0
);
CREATE INDEX talkgroups_system_tgid_idx ON talkgroups (system_id, tgid);
CREATE INDEX IF NOT EXISTS talkgroup_id_tags ON talkgroups USING GIN (tags);
CREATE TABLE IF NOT EXISTS talkgroups_learned(
id SERIAL PRIMARY KEY,
system_id INTEGER REFERENCES systems(id) NOT NULL,
tgid INTEGER NOT NULL,
name TEXT NOT NULL,
alpha_tag TEXT,
ignored BOOLEAN,
UNIQUE (system_id, tgid, name)
);
CREATE TABLE IF NOT EXISTS alerts(
id UUID PRIMARY KEY,
time TIMESTAMPTZ NOT NULL,
tgid INTEGER NOT NULL,
system_id INTEGER REFERENCES systems(id) NOT NULL,
weight REAL,
score REAL,
orig_score REAL,
notified BOOLEAN NOT NULL DEFAULT 'false',
metadata JSONB
);
CREATE OR REPLACE FUNCTION learn_talkgroup()
RETURNS TRIGGER AS $$
BEGIN
IF NOT EXISTS (
SELECT tg.system_id, tg.tgid, tg.name, tg.alpha_tag FROM talkgroups tg WHERE tg.system_id = NEW.system AND tg.tgid = NEW.talkgroup
UNION
SELECT tgl.system_id, tgl.tgid, tgl.name, tgl.alpha_tag FROM talkgroups_learned tgl WHERE tgl.system_id = NEW.system AND tgl.tgid = NEW.talkgroup
) THEN
INSERT INTO talkgroups_learned(system_id, tgid, name, alpha_tag) VALUES(
NEW.system, NEW.talkgroup, NEW.tg_label, NEW.tg_alpha_tag
) ON CONFLICT DO NOTHING;
END IF;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS calls(
id UUID PRIMARY KEY,
submitter INTEGER REFERENCES api_keys(id) ON DELETE SET NULL,
system INTEGER NOT NULL,
talkgroup INTEGER NOT NULL,
call_date TIMESTAMPTZ NOT NULL,
audio_name TEXT,
audio_blob BYTEA,
duration INTEGER,
audio_type TEXT,
audio_url TEXT,
frequency INTEGER NOT NULL,
frequencies INTEGER[],
patches INTEGER[],
tg_label TEXT,
tg_alpha_tag TEXT,
tg_group TEXT,
source INTEGER NOT NULL,
transcript TEXT
);
CREATE OR REPLACE TRIGGER learn_tg AFTER INSERT ON calls
FOR EACH ROW EXECUTE FUNCTION learn_talkgroup();
CREATE INDEX IF NOT EXISTS calls_transcript_idx ON calls USING GIN (to_tsvector('english', transcript));
CREATE INDEX IF NOT EXISTS calls_call_date_tg_idx ON calls(system, talkgroup, call_date);
CREATE TABLE IF NOT EXISTS settings(
name TEXT PRIMARY KEY,
updated_by INTEGER REFERENCES users(id),
value JSONB
);
CREATE TABLE IF NOT EXISTS incidents(
id UUID PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
start_time TIMESTAMP,
end_time TIMESTAMP,
location JSONB,
metadata JSONB
);
CREATE INDEX IF NOT EXISTS incidents_name_description_idx ON incidents USING GIN (
(to_tsvector('english', name) || to_tsvector('english', coalesce(description, ''))
)
);
CREATE TABLE IF NOT EXISTS incidents_calls(
incident_id UUID REFERENCES incidents(id) ON UPDATE CASCADE ON DELETE CASCADE,
call_id UUID REFERENCES calls(id) ON UPDATE CASCADE,
notes JSONB,
PRIMARY KEY (incident_id, call_id)
);

View file

@ -8,25 +8,21 @@ WHERE tags && ARRAY[$1];
-- name: GetTalkgroupIDsByTags :many -- name: GetTalkgroupIDsByTags :many
SELECT system_id, tgid FROM talkgroups SELECT system_id, tgid FROM talkgroups
WHERE (tags @> ARRAY[sqlc.arg(anyTags)]) WHERE (tags @> ARRAY[@anyTags])
AND (tags && ARRAY[sqlc.arg(allTags)]) AND (tags && ARRAY[@allTags])
AND NOT (tags @> ARRAY[sqlc.arg(notTags)]); AND NOT (tags @> ARRAY[@notTags]);
-- name: GetTalkgroupTags :one -- name: GetTalkgroupTags :one
SELECT tags FROM talkgroups SELECT tags FROM talkgroups
WHERE id = systg2id($1, $2); WHERE system_id = @system_id AND tgid = @tg_id;
-- name: SetTalkgroupTags :exec -- name: SetTalkgroupTags :exec
UPDATE talkgroups SET tags = $3 UPDATE talkgroups SET tags = @tags
WHERE id = systg2id($1, $2); WHERE system_id = @system_id AND tgid = @tg_id;
-- name: BulkSetTalkgroupTags :exec
UPDATE talkgroups SET tags = $2
WHERE id = ANY($1);
-- name: GetTalkgroup :one -- name: GetTalkgroup :one
SELECT sqlc.embed(talkgroups) FROM talkgroups SELECT sqlc.embed(talkgroups) FROM talkgroups
WHERE (system_id, tgid) = (@system_id, @tgid); WHERE (system_id, tgid) = (@system_id, @tg_id);
-- name: GetTalkgroupWithLearned :one -- name: GetTalkgroupWithLearned :one
SELECT SELECT
@ -34,17 +30,17 @@ sqlc.embed(tg), sqlc.embed(sys),
FALSE learned FALSE learned
FROM talkgroups tg FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id JOIN systems sys ON tg.system_id = sys.id
WHERE (tg.system_id, tg.tgid) = (sqlc.arg(system_id), sqlc.arg(tgid)) WHERE (tg.system_id, tg.tgid) = (@system_id, @tgid)
UNION UNION
SELECT SELECT
tgl.id::INT8, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name, NULL::UUID, tgl.system_id::INT4, tgl.tgid::INT4, tgl.name,
tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB, tgl.alpha_tag, tgl.alpha_tag, NULL::INTEGER, NULL::JSONB,
CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END, CASE WHEN tgl.alpha_tag IS NULL THEN NULL ELSE ARRAY[tgl.alpha_tag] END,
TRUE, NULL::JSONB, 1.0, sys.id, sys.name, TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
TRUE learned TRUE learned
FROM talkgroups_learned tgl FROM talkgroups_learned tgl
JOIN systems sys ON tgl.system_id = sys.id JOIN systems sys ON tgl.system_id = sys.id
WHERE tgl.system_id = sqlc.arg(system_id) AND tgl.tgid = sqlc.arg(tgid) AND ignored IS NOT TRUE; WHERE tgl.system_id = @system_id AND tgl.tgid = @tgid AND ignored IS NOT TRUE;
-- name: GetTalkgroupsWithLearnedBySystem :many -- name: GetTalkgroupsWithLearnedBySystem :many
SELECT SELECT
@ -82,7 +78,7 @@ JOIN systems sys ON tgl.system_id = sys.id
WHERE ignored IS NOT TRUE; WHERE ignored IS NOT TRUE;
-- name: GetSystemName :one -- name: GetSystemName :one
SELECT name FROM systems WHERE id = sqlc.arg(system_id); SELECT name FROM systems WHERE id = @system_id;
-- name: UpdateTalkgroup :one -- name: UpdateTalkgroup :one
UPDATE talkgroups UPDATE talkgroups