diff --git a/pkg/calls/filter.go b/pkg/calls/filter.go new file mode 100644 index 0000000..097862e --- /dev/null +++ b/pkg/calls/filter.go @@ -0,0 +1,90 @@ +package calls + +type filterQuery struct { + Query string + Params []interface{} +} + +type Talkgroup struct { + System uint32 + Talkgroup uint32 +} + +func (t Talkgroup) Pack() int64 { + // P25 system IDs are 12 bits, so we can fit them in a signed 8 byte int (int64, pg INT8) + return int64((int64(t.System) << 32) | int64(t.Talkgroup)) +} + +type Filter struct { + Talkgroups []Talkgroup `json:"talkgroups"` + TalkgroupsNot []Talkgroup `json:"talkgroupsNot"` + TalkgroupTagsAll []string `json:"talkgroupTagsAll"` + TalkgroupTagsAny []string `json:"talkgroupTagsAny"` + TalkgroupTagsNot []string `json:"talkgroupTagsNot"` + + talkgroups map[Talkgroup]bool + talkgroupTagsAll map[string]bool + talkgroupTagsAny map[string]bool + talkgroupTagsNot map[string]bool + + query *filterQuery +} + +func queryParams(s string, p ...any) (string, []any) { + return s, p +} + +func (f *Filter) filterQuery() filterQuery { + var q string + var args []interface{} + + q, args = queryParams( + `((talkgroups.id = ANY(?) OR talkgroups.tags @> ARRAY[?]) OR (talkgroups.tags && ARRAY[?])) AND (talkgroups.id != ANY(?) AND NOT talkgroups.tags @> ARRAY[?])`, + f.Talkgroups, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupsNot, f.TalkgroupTagsNot) + + return filterQuery{Query: q, Params: args} +} + +func (f *Filter) Packed(tg []Talkgroup) []int64 { + s := make([]int64, len(f.Talkgroups)) + + for i, v := range tg { + s[i] = v.Pack() + } + + return s +} + +func (f *Filter) Compile() *Filter { + f.talkgroups = make(map[Talkgroup]bool) + for _, tg := range f.Talkgroups { + f.talkgroups[tg] = true + } + + for _, tg := range f.TalkgroupsNot { + f.talkgroups[tg] = false + } + + f.talkgroupTagsAll = make(map[string]bool) + for _, tag := range f.TalkgroupTagsAll { + f.talkgroupTagsAll[tag] = true + } + + f.talkgroupTagsAny = make(map[string]bool) + for _, tag := range f.TalkgroupTagsAny { + f.talkgroupTagsAny[tag] = true + } + + for _, tag := range f.TalkgroupTagsNot { + f.talkgroupTagsNot[tag] = true + } + + q := f.filterQuery() + f.query = &q + + return f +} + +type FilterCache struct { + cache map[string]Filter +} diff --git a/pkg/gordio/database/database.go b/pkg/gordio/database/database.go index 4ed9e61..99c26fc 100644 --- a/pkg/gordio/database/database.go +++ b/pkg/gordio/database/database.go @@ -13,6 +13,8 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) +// This file will eventually turn into a postgres driver. + // DB is a database handle. type DB struct { *pgxpool.Pool diff --git a/pkg/gordio/database/models.go b/pkg/gordio/database/models.go index 6aafd8d..da5b3e2 100644 --- a/pkg/gordio/database/models.go +++ b/pkg/gordio/database/models.go @@ -68,8 +68,9 @@ type System struct { } type Talkgroup struct { + ID int64 `json:"id"` SystemID int `json:"system_id"` - Tgid int `json:"tgid"` + Tgid *int32 `json:"tgid"` Name *string `json:"name"` TgGroup *string `json:"tg_group"` Frequency *int32 `json:"frequency"` diff --git a/pkg/gordio/database/querier.go b/pkg/gordio/database/querier.go index ab6d283..8f35eb3 100644 --- a/pkg/gordio/database/querier.go +++ b/pkg/gordio/database/querier.go @@ -13,12 +13,13 @@ import ( type Querier interface { AddCall(ctx context.Context, arg AddCallParams) (uuid.UUID, error) + BulkSetTalkgroupTags(ctx context.Context, iD int64, tags []string) error CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) DeleteAPIKey(ctx context.Context, apiKey string) error DeleteUser(ctx context.Context, username string) error GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error) - GetTalkgroupTags(ctx context.Context, systemID int, tgid int) ([]string, error) + GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ([]Talkgroup, error) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]Talkgroup, error) GetUserByID(ctx context.Context, id int32) (User, error) @@ -26,7 +27,7 @@ type Querier interface { GetUserByUsername(ctx context.Context, username string) (User, error) GetUsers(ctx context.Context) ([]User, error) SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error - SetTalkgroupTags(ctx context.Context, tags []string, tgid int) error + SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []string) error UpdatePassword(ctx context.Context, username string, password string) error } diff --git a/pkg/gordio/database/talkgroups.sql.go b/pkg/gordio/database/talkgroups.sql.go index 398d77b..ce66dcf 100644 --- a/pkg/gordio/database/talkgroups.sql.go +++ b/pkg/gordio/database/talkgroups.sql.go @@ -9,20 +9,30 @@ import ( "context" ) -const getTalkgroupTags = `-- name: GetTalkgroupTags :one -SELECT tags FROM talkgroups -WHERE system_id = $1 AND tgid = $2 +const bulkSetTalkgroupTags = `-- name: BulkSetTalkgroupTags :exec +UPDATE talkgroups SET tags = $2 +WHERE id = ANY($1) ` -func (q *Queries) GetTalkgroupTags(ctx context.Context, systemID int, tgid int) ([]string, error) { - row := q.db.QueryRow(ctx, getTalkgroupTags, systemID, tgid) +func (q *Queries) BulkSetTalkgroupTags(ctx context.Context, iD int64, tags []string) error { + _, err := q.db.Exec(ctx, bulkSetTalkgroupTags, iD, tags) + return err +} + +const getTalkgroupTags = `-- name: GetTalkgroupTags :one +SELECT tags FROM talkgroups +WHERE id = systg2id($1, $2) +` + +func (q *Queries) GetTalkgroupTags(ctx context.Context, sys int, tg int) ([]string, error) { + row := q.db.QueryRow(ctx, getTalkgroupTags, sys, tg) var tags []string err := row.Scan(&tags) return tags, err } const getTalkgroupsWithAllTags = `-- name: GetTalkgroupsWithAllTags :many -SELECT system_id, tgid, name, tg_group, frequency, metadata, tags FROM talkgroups +SELECT id, system_id, tgid, name, tg_group, frequency, metadata, tags FROM talkgroups WHERE tags && ARRAY[$1] ` @@ -36,6 +46,7 @@ func (q *Queries) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ( for rows.Next() { var i Talkgroup if err := rows.Scan( + &i.ID, &i.SystemID, &i.Tgid, &i.Name, @@ -55,7 +66,7 @@ func (q *Queries) GetTalkgroupsWithAllTags(ctx context.Context, tags []string) ( } const getTalkgroupsWithAnyTags = `-- name: GetTalkgroupsWithAnyTags :many -SELECT system_id, tgid, name, tg_group, frequency, metadata, tags FROM talkgroups +SELECT id, system_id, tgid, name, tg_group, frequency, metadata, tags FROM talkgroups WHERE tags @> ARRAY[$1] ` @@ -69,6 +80,7 @@ func (q *Queries) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ( for rows.Next() { var i Talkgroup if err := rows.Scan( + &i.ID, &i.SystemID, &i.Tgid, &i.Name, @@ -88,11 +100,11 @@ func (q *Queries) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ( } const setTalkgroupTags = `-- name: SetTalkgroupTags :exec -UPDATE talkgroups SET tags = $1 -WHERE system_id = $1 AND tgid = $2 +UPDATE talkgroups SET tags = $3 +WHERE id = systg2id($1, $2) ` -func (q *Queries) SetTalkgroupTags(ctx context.Context, tags []string, tgid int) error { - _, err := q.db.Exec(ctx, setTalkgroupTags, tags, tgid) +func (q *Queries) SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []string) error { + _, err := q.db.Exec(ctx, setTalkgroupTags, sys, tg, tags) return err } diff --git a/pkg/pb/stillbox.pb.go b/pkg/pb/stillbox.pb.go index 22a2096..b0ca97c 100644 --- a/pkg/pb/stillbox.pb.go +++ b/pkg/pb/stillbox.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.27.1 +// protoc v5.27.2 // source: stillbox.proto package pb diff --git a/sql/postgres/migrations/001_initial.up.sql b/sql/postgres/migrations/001_initial.up.sql index ee5853b..3aca0a8 100644 --- a/sql/postgres/migrations/001_initial.up.sql +++ b/sql/postgres/migrations/001_initial.up.sql @@ -23,19 +23,42 @@ CREATE TABLE IF NOT EXISTS systems( 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( - system_id INTEGER REFERENCES systems(id) NOT NULL, - tgid INTEGER, + id INT8 PRIMARY KEY, + system_id INTEGER REFERENCES systems(id) NOT NULL GENERATED ALWAYS AS (id >> 32) STORED, + tgid INTEGER GENERATED ALWAYS AS (id & x'ffffffff'::BIGINT) STORED, name TEXT, tg_group TEXT, frequency INTEGER, metadata JSONB, - tags TEXT[] NOT NULL DEFAULT '{}', - PRIMARY KEY (system_id, tgid) + tags TEXT[] NOT NULL DEFAULT '{}' ); 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, diff --git a/sql/postgres/queries/talkgroups.sql b/sql/postgres/queries/talkgroups.sql index fa224a3..213d074 100644 --- a/sql/postgres/queries/talkgroups.sql +++ b/sql/postgres/queries/talkgroups.sql @@ -8,8 +8,12 @@ WHERE tags && ARRAY[$1]; -- name: GetTalkgroupTags :one SELECT tags FROM talkgroups -WHERE system_id = $1 AND tgid = $2; +WHERE id = systg2id($1, $2); -- name: SetTalkgroupTags :exec -UPDATE talkgroups SET tags = $1 -WHERE system_id = $1 AND tgid = $2; +UPDATE talkgroups SET tags = $3 +WHERE id = systg2id($1, $2); + +-- name: BulkSetTalkgroupTags :exec +UPDATE talkgroups SET tags = $2 +WHERE id = ANY($1);