Skip to content

Commit ad5cefd

Browse files
Copilotneongreen
andcommitted
tak: Fix created_at and switch to numeric IDs with installation suffix
Bug fixes: 1. Store actual creation timestamp in created_at field (was showing 1970) 2. Switch from ULID to numeric IDs: tak-<number>-<suffix> format - Each installation gets a unique random 6-char suffix - Suffix is stored in database metadata and reused - Allows parallel installations with same numeric ID but different suffixes Changes: - Add created_at timestamp to Event struct and database - Store creation time from time.Now() instead of converting Lamport TS - Add metadata table for installation suffix storage - Add task_counter table for numeric ID sequence - Generate IDs like tak-1-abc123, tak-2-abc123, etc. - Update all tests to use new ID format and include CreatedAt Co-authored-by: neongreen <1523306+neongreen@users.noreply.github.com>
1 parent 0ad0f0c commit ad5cefd

File tree

7 files changed

+246
-165
lines changed

7 files changed

+246
-165
lines changed

tak/db.go

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package main
22

33
import (
4+
"crypto/rand"
45
"database/sql"
56
"encoding/json"
67
"fmt"
8+
"math/big"
79
"os"
810
"path/filepath"
11+
"time"
912

1013
_ "modernc.org/sqlite"
1114
)
@@ -47,12 +50,13 @@ func OpenDB(path string) (*DB, error) {
4750
return &DB{db: db}, nil
4851
}
4952

50-
// InitDB creates the events table
53+
// InitDB creates the events table and metadata table
5154
func (d *DB) InitDB() error {
5255
schema := `
5356
CREATE TABLE IF NOT EXISTS events (
5457
id TEXT PRIMARY KEY,
5558
ts INTEGER NOT NULL,
59+
created_at INTEGER NOT NULL,
5660
actor TEXT NOT NULL,
5761
role TEXT NOT NULL,
5862
kind TEXT NOT NULL,
@@ -65,23 +69,44 @@ func (d *DB) InitDB() error {
6569
);
6670
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts, id);
6771
CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);
72+
73+
CREATE TABLE IF NOT EXISTS metadata (
74+
key TEXT PRIMARY KEY,
75+
value TEXT NOT NULL
76+
);
77+
78+
CREATE TABLE IF NOT EXISTS task_counter (
79+
last_id INTEGER NOT NULL
80+
);
6881
`
6982

7083
if _, err := d.db.Exec(schema); err != nil {
7184
return fmt.Errorf("failed to create schema: %w", err)
7285
}
7386

87+
// Initialize counter if it doesn't exist
88+
var count int
89+
err := d.db.QueryRow("SELECT COUNT(*) FROM task_counter").Scan(&count)
90+
if err != nil {
91+
return fmt.Errorf("failed to check counter: %w", err)
92+
}
93+
if count == 0 {
94+
if _, err := d.db.Exec("INSERT INTO task_counter (last_id) VALUES (0)"); err != nil {
95+
return fmt.Errorf("failed to initialize counter: %w", err)
96+
}
97+
}
98+
7499
return nil
75100
}
76101

77102
// InsertEvent adds an event to the database
78103
func (d *DB) InsertEvent(e Event) error {
79104
query := `
80-
INSERT INTO events (id, ts, actor, role, kind, payload, ctx, repo_uuid, branch, commit_sha, jj_op_id)
81-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
105+
INSERT INTO events (id, ts, created_at, actor, role, kind, payload, ctx, repo_uuid, branch, commit_sha, jj_op_id)
106+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
82107
`
83108

84-
_, err := d.db.Exec(query, e.ID, e.TS, e.Actor, e.Role, e.Kind, e.Payload, e.Ctx, e.RepoUUID, e.Branch, e.Commit, e.JJOpID)
109+
_, err := d.db.Exec(query, e.ID, e.TS, e.CreatedAt.UnixNano(), e.Actor, e.Role, e.Kind, e.Payload, e.Ctx, e.RepoUUID, e.Branch, e.Commit, e.JJOpID)
85110
if err != nil {
86111
return fmt.Errorf("failed to insert event: %w", err)
87112
}
@@ -91,7 +116,7 @@ func (d *DB) InsertEvent(e Event) error {
91116

92117
// GetEvents retrieves all events in chronological order
93118
func (d *DB) GetEvents() ([]Event, error) {
94-
query := `SELECT id, ts, actor, role, kind, payload, ctx, repo_uuid, branch, commit_sha, jj_op_id
119+
query := `SELECT id, ts, created_at, actor, role, kind, payload, ctx, repo_uuid, branch, commit_sha, jj_op_id
95120
FROM events ORDER BY ts, id`
96121

97122
rows, err := d.db.Query(query)
@@ -104,12 +129,14 @@ func (d *DB) GetEvents() ([]Event, error) {
104129
for rows.Next() {
105130
var e Event
106131
var ctx, repoUUID, branch, commit, jjOpID sql.NullString
132+
var createdAtNano int64
107133

108-
err := rows.Scan(&e.ID, &e.TS, &e.Actor, &e.Role, &e.Kind, &e.Payload, &ctx, &repoUUID, &branch, &commit, &jjOpID)
134+
err := rows.Scan(&e.ID, &e.TS, &createdAtNano, &e.Actor, &e.Role, &e.Kind, &e.Payload, &ctx, &repoUUID, &branch, &commit, &jjOpID)
109135
if err != nil {
110136
return nil, fmt.Errorf("failed to scan event row: %w", err)
111137
}
112138

139+
e.CreatedAt = time.Unix(0, createdAtNano)
113140
if ctx.Valid {
114141
e.Ctx = json.RawMessage(ctx.String)
115142
}
@@ -135,7 +162,7 @@ func (d *DB) GetEvents() ([]Event, error) {
135162
// GetEventsByTaskID retrieves events for a specific task
136163
func (d *DB) GetEventsByTaskID(taskID string) ([]Event, error) {
137164
query := `
138-
SELECT id, ts, actor, role, kind, payload, ctx, repo_uuid, branch, commit_sha, jj_op_id
165+
SELECT id, ts, created_at, actor, role, kind, payload, ctx, repo_uuid, branch, commit_sha, jj_op_id
139166
FROM events
140167
WHERE json_extract(payload, '$.task_id') = ?
141168
ORDER BY ts, id
@@ -151,12 +178,14 @@ func (d *DB) GetEventsByTaskID(taskID string) ([]Event, error) {
151178
for rows.Next() {
152179
var e Event
153180
var ctx, repoUUID, branch, commit, jjOpID sql.NullString
181+
var createdAtNano int64
154182

155-
err := rows.Scan(&e.ID, &e.TS, &e.Actor, &e.Role, &e.Kind, &e.Payload, &ctx, &repoUUID, &branch, &commit, &jjOpID)
183+
err := rows.Scan(&e.ID, &e.TS, &createdAtNano, &e.Actor, &e.Role, &e.Kind, &e.Payload, &ctx, &repoUUID, &branch, &commit, &jjOpID)
156184
if err != nil {
157185
return nil, fmt.Errorf("failed to scan event row: %w", err)
158186
}
159187

188+
e.CreatedAt = time.Unix(0, createdAtNano)
160189
if ctx.Valid {
161190
e.Ctx = json.RawMessage(ctx.String)
162191
}
@@ -200,3 +229,68 @@ func DBExists(path string) bool {
200229
_, err := os.Stat(path)
201230
return err == nil
202231
}
232+
233+
// GetOrCreateInstallationSuffix gets the installation suffix or creates one if it doesn't exist
234+
func (d *DB) GetOrCreateInstallationSuffix() (string, error) {
235+
// Try to get existing suffix
236+
var suffix string
237+
err := d.db.QueryRow("SELECT value FROM metadata WHERE key = 'installation_suffix'").Scan(&suffix)
238+
if err == nil {
239+
return suffix, nil
240+
}
241+
if err != sql.ErrNoRows {
242+
return "", fmt.Errorf("failed to query installation suffix: %w", err)
243+
}
244+
245+
// Generate new suffix (6 random alphanumeric characters)
246+
suffix = generateRandomSuffix(6)
247+
248+
// Store it
249+
_, err = d.db.Exec("INSERT INTO metadata (key, value) VALUES ('installation_suffix', ?)", suffix)
250+
if err != nil {
251+
return "", fmt.Errorf("failed to store installation suffix: %w", err)
252+
}
253+
254+
return suffix, nil
255+
}
256+
257+
// GetNextTaskNumber gets the next task number and increments the counter
258+
func (d *DB) GetNextTaskNumber() (int64, error) {
259+
tx, err := d.db.Begin()
260+
if err != nil {
261+
return 0, fmt.Errorf("failed to begin transaction: %w", err)
262+
}
263+
defer tx.Rollback()
264+
265+
var lastID int64
266+
err = tx.QueryRow("SELECT last_id FROM task_counter").Scan(&lastID)
267+
if err != nil {
268+
return 0, fmt.Errorf("failed to get last task ID: %w", err)
269+
}
270+
271+
nextID := lastID + 1
272+
_, err = tx.Exec("UPDATE task_counter SET last_id = ?", nextID)
273+
if err != nil {
274+
return 0, fmt.Errorf("failed to update task counter: %w", err)
275+
}
276+
277+
if err := tx.Commit(); err != nil {
278+
return 0, fmt.Errorf("failed to commit transaction: %w", err)
279+
}
280+
281+
return nextID, nil
282+
}
283+
284+
// generateRandomSuffix generates a random alphanumeric suffix
285+
func generateRandomSuffix(length int) string {
286+
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
287+
b := make([]byte, length)
288+
for i := range b {
289+
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
290+
if err != nil {
291+
panic(err)
292+
}
293+
b[i] = charset[n.Int64()]
294+
}
295+
return string(b)
296+
}

tak/main.go

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/user"
88
"strings"
9+
"time"
910

1011
"github.com/spf13/cobra"
1112
)
@@ -86,7 +87,10 @@ var newCmd = &cobra.Command{
8687
return err
8788
}
8889

89-
taskID := GenerateULID("tak")
90+
taskID, err := GenerateTaskID(db)
91+
if err != nil {
92+
return err
93+
}
9094
eventID := GenerateEventID()
9195

9296
payload := TaskCreatedPayload{
@@ -99,13 +103,15 @@ var newCmd = &cobra.Command{
99103
return fmt.Errorf("failed to marshal payload: %w", err)
100104
}
101105

106+
now := time.Now()
102107
event := Event{
103-
ID: eventID,
104-
TS: GetNextLamportTS(),
105-
Actor: currentUser,
106-
Role: "human",
107-
Kind: "task.created",
108-
Payload: payloadJSON,
108+
ID: eventID,
109+
TS: GetNextLamportTS(),
110+
CreatedAt: now,
111+
Actor: currentUser,
112+
Role: "human",
113+
Kind: "task.created",
114+
Payload: payloadJSON,
109115
}
110116

111117
if err := db.InsertEvent(event); err != nil {
@@ -157,13 +163,15 @@ var statusSetCmd = &cobra.Command{
157163
return fmt.Errorf("failed to marshal payload: %w", err)
158164
}
159165

166+
now := time.Now()
160167
event := Event{
161-
ID: eventID,
162-
TS: GetNextLamportTS(),
163-
Actor: currentUser,
164-
Role: role,
165-
Kind: "task.status.set",
166-
Payload: payloadJSON,
168+
ID: eventID,
169+
TS: GetNextLamportTS(),
170+
CreatedAt: now,
171+
Actor: currentUser,
172+
Role: role,
173+
Kind: "task.status.set",
174+
Payload: payloadJSON,
167175
}
168176

169177
if err := db.InsertEvent(event); err != nil {
@@ -205,13 +213,15 @@ var noteCmd = &cobra.Command{
205213
return fmt.Errorf("failed to marshal payload: %w", err)
206214
}
207215

216+
now := time.Now()
208217
event := Event{
209-
ID: eventID,
210-
TS: GetNextLamportTS(),
211-
Actor: currentUser,
212-
Role: "human",
213-
Kind: "task.note.add",
214-
Payload: payloadJSON,
218+
ID: eventID,
219+
TS: GetNextLamportTS(),
220+
CreatedAt: now,
221+
Actor: currentUser,
222+
Role: "human",
223+
Kind: "task.note.add",
224+
Payload: payloadJSON,
215225
}
216226

217227
if err := db.InsertEvent(event); err != nil {

tak/reducer.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"fmt"
66
"sort"
7-
"time"
87
)
98

109
// Reducer reconstructs task state from events
@@ -45,7 +44,7 @@ func (r *Reducer) applyTaskCreated(e Event) error {
4544
Axes: make(map[string]AxisStatus),
4645
Notes: []Note{},
4746
CreatedBy: payload.CreatedBy,
48-
CreatedAt: time.Unix(0, e.TS*1e6), // Convert Lamport TS to approximate time
47+
CreatedAt: e.CreatedAt, // Use actual creation time from event
4948
}
5049

5150
return nil
@@ -101,7 +100,7 @@ func (r *Reducer) applyTaskNoteAdd(e Event) error {
101100
note := Note{
102101
Markdown: payload.Markdown,
103102
Actor: e.Actor,
104-
Timestamp: time.Unix(0, e.TS*1e6),
103+
Timestamp: e.CreatedAt, // Use actual creation time from event
105104
}
106105
task.Notes = append(task.Notes, note)
107106

0 commit comments

Comments
 (0)