package sqlite import ( "database/sql" "encoding/json" "fmt" "time" ) // Revision is a versioned snapshot of an entity. type Revision struct { ID int64 `json:"id"` EntityType string `json:"entityType"` EntityID string `json:"entityId"` ProjectID string `json:"projectId"` Rev int `json:"rev"` At time.Time `json:"at"` ActorUserID string `json:"actorUserId,omitempty"` Op string `json:"op"` Snapshot json.RawMessage `json:"snapshot"` } // AppendRevision stores a new revision snapshot. func (db *DB) AppendRevision(entityType, entityID, projectID, op, actorUserID string, snapshot any) (Revision, error) { data, err := json.Marshal(snapshot) if err != nil { return Revision{}, err } var maxRev sql.NullInt64 _ = db.SQL.QueryRow(`SELECT MAX(rev) FROM revisions WHERE entity_type = ? AND entity_id = ?`, entityType, entityID).Scan(&maxRev) rev := 1 if maxRev.Valid { rev = int(maxRev.Int64) + 1 } at := time.Now().UTC() var actor any if actorUserID != "" { actor = actorUserID } res, err := db.SQL.Exec(` INSERT INTO revisions(entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json) VALUES(?,?,?,?,?,?,?,?)`, entityType, entityID, projectID, rev, formatTime(at), actor, op, string(data)) if err != nil { return Revision{}, err } id, _ := res.LastInsertId() return Revision{ ID: id, EntityType: entityType, EntityID: entityID, ProjectID: projectID, Rev: rev, At: at, ActorUserID: actorUserID, Op: op, Snapshot: data, }, nil } // ListRevisions returns revisions for an entity, newest first. func (db *DB) ListRevisions(entityType, entityID string) ([]Revision, error) { rows, err := db.SQL.Query(` SELECT id, entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json FROM revisions WHERE entity_type = ? AND entity_id = ? ORDER BY rev DESC`, entityType, entityID) if err != nil { return nil, err } defer rows.Close() return scanRevisions(rows) } // GetRevision returns one revision by id. func (db *DB) GetRevision(id int64) (Revision, error) { row := db.SQL.QueryRow(` SELECT id, entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json FROM revisions WHERE id = ?`, id) var r Revision var at string var actor sql.NullString var snap string err := row.Scan(&r.ID, &r.EntityType, &r.EntityID, &r.ProjectID, &r.Rev, &at, &actor, &r.Op, &snap) if err == sql.ErrNoRows { return Revision{}, fmt.Errorf("revision %d: not found", id) } if err != nil { return Revision{}, err } r.At = parseTime(at) if actor.Valid { r.ActorUserID = actor.String } r.Snapshot = json.RawMessage(snap) return r, nil } func scanRevisions(rows *sql.Rows) ([]Revision, error) { var out []Revision for rows.Next() { var r Revision var at string var actor sql.NullString var snap string if err := rows.Scan(&r.ID, &r.EntityType, &r.EntityID, &r.ProjectID, &r.Rev, &at, &actor, &r.Op, &snap); err != nil { return nil, err } r.At = parseTime(at) if actor.Valid { r.ActorUserID = actor.String } r.Snapshot = json.RawMessage(snap) out = append(out, r) } if out == nil { out = []Revision{} } return out, rows.Err() }