package store import ( "context" "fmt" "time" ) // Event — a normalized, derived observation from a fact. An event has an action // (what happened, e.g. "refill") and an object (what it happened to, e.g. // "cat_water_fountain"). Events are lossy — not every fact produces one. // They are the input to the pattern detector. type Event struct { ID int64 FactID int64 Action string Object string Ts time.Time } // CreateEvent persists a derived event from a fact. Best-effort: the caller // (the pattern extractor) decides whether the fact warrants an event; this // just writes the row. Not every fact produces an event. func (s *Store) CreateEvent(ctx context.Context, factID int64, action, object string, ts time.Time) (int64, error) { res, err := s.db.ExecContext(ctx, `INSERT INTO events (fact_id, action, object, ts) VALUES (?,?,?,?)`, factID, action, object, ts.UnixMilli()) if err != nil { return 0, fmt.Errorf("create event: %w", err) } id, err := res.LastInsertId() if err != nil { return 0, fmt.Errorf("create event: last insert id: %w", err) } return id, nil } // EventPair identifies one action+object grouping in the events table — the // unit the pattern detector reasons about. type EventPair struct { Action string Object string } // DistinctEventPairs returns every distinct action+object pair that has at // least one event, in no particular order. This is what lets the proactive // digestion tick run the pattern detector over everything accumulated so far // instead of only the pair touched by the utterance that just landed // (Vikunja #43) — the tick has no "current utterance," so it has to ask the // store what to look at. func (s *Store) DistinctEventPairs(ctx context.Context) ([]EventPair, error) { rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT action, object FROM events`) if err != nil { return nil, fmt.Errorf("distinct event pairs: %w", err) } defer rows.Close() var out []EventPair for rows.Next() { var p EventPair if err := rows.Scan(&p.Action, &p.Object); err != nil { return nil, err } out = append(out, p) } return out, rows.Err() } // EventsFor returns all events matching action+object, ordered by ts ascending // (oldest first — the order the pattern detector needs for interval computation). func (s *Store) EventsFor(ctx context.Context, action, object string) ([]Event, error) { rows, err := s.db.QueryContext(ctx, ` SELECT id, fact_id, action, object, ts FROM events WHERE action = ? AND object = ? ORDER BY ts ASC, id ASC`, action, object) if err != nil { return nil, fmt.Errorf("events for %s/%s: %w", action, object, err) } defer rows.Close() var out []Event for rows.Next() { var e Event var tsMillis int64 if err := rows.Scan(&e.ID, &e.FactID, &e.Action, &e.Object, &tsMillis); err != nil { return nil, err } e.Ts = time.UnixMilli(tsMillis).UTC() out = append(out, e) } return out, rows.Err() }