返回 DeepSeek-Reasonix
upsert.go
1 package sessioncatalog
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 )
8
9 func (c *Catalog) UpsertSession(ctx context.Context, record SessionRecord) error {
10 record = normalizeSessionRecord(record)
11 record.enqueueSequence = c.mutationSeq.Add(1)
12 return c.upsertSessions(ctx, []SessionRecord{record}, nil, "write")
13 }
14
15 func (c *Catalog) upsertSessions(ctx context.Context, records []SessionRecord, generations map[string]int64, reason string) error {
16 _, err := c.upsertSessionsWithNotification(ctx, records, generations, reason, true, upsertExactSource)
17 return err
18 }
19
20 func (c *Catalog) upsertExactPathSession(ctx context.Context, record SessionRecord) (bool, error) {
21 dirty, err := c.upsertSessionsWithNotification(ctx, []SessionRecord{record}, nil, "write", true, upsertExactSource)
22 return len(dirty) > 0, err
23 }
24
25 func (c *Catalog) upsertSessionsWithNotification(ctx context.Context, records []SessionRecord, generations map[string]int64, reason string, notify bool, mode sessionUpsertMode) (map[string]DirectoryTarget, error) {
26 dirtyDirectories := map[string]DirectoryTarget{}
27 if len(records) == 0 {
28 return dirtyDirectories, nil
29 }
30 c.mutationMu.Lock()
31 defer c.mutationMu.Unlock()
32 filtered := records[:0]
33 for _, record := range records {
34 pathKey := c.pathKey(record.Path)
35 if c.pathMutationAllowed(pathKey, record.enqueueSequence) {
36 filtered = append(filtered, record)
37 }
38 }
39 records = filtered
40 if len(records) == 0 {
41 return dirtyDirectories, nil
42 }
43 if mode == upsertExactSource {
44 prepared := make([]SessionRecord, 0, len(records))
45 for _, raw := range records {
46 record, skip, projectionDirty, err := c.prepareExactPathProjection(ctx, raw)
47 if err != nil {
48 return dirtyDirectories, err
49 }
50 if projectionDirty {
51 dirtyDirectories[c.pathKey(record.Directory)] = DirectoryTarget{
52 Path: record.Directory, Scope: record.Scope, WorkspaceRoot: record.WorkspaceRoot,
53 }
54 }
55 if !skip {
56 prepared = append(prepared, record)
57 }
58 }
59 records = prepared
60 if len(records) == 0 {
61 return dirtyDirectories, nil
62 }
63 }
64 tx, err := c.db.BeginTx(ctx, nil)
65 if err != nil {
66 return dirtyDirectories, err
67 }
68 affected := map[TopicKey]struct{}{}
69 roots := map[string]struct{}{}
70 directoryGenerations := map[string]int64{}
71 for _, raw := range records {
72 record := normalizeSessionRecord(raw)
73 pathKey := c.pathKey(record.Path)
74 directoryKey := c.pathKey(record.Directory)
75 remapped, err := removeRemappedSessionIdentity(ctx, tx, record.Path, pathKey)
76 if err != nil {
77 _ = tx.Rollback()
78 return dirtyDirectories, err
79 }
80 for _, key := range remapped {
81 affected[key] = struct{}{}
82 }
83 var previous TopicKey
84 if err := tx.QueryRowContext(ctx, `SELECT scope,workspace_root,workspace_root_key,topic_id FROM catalog_sessions WHERE path_key=?`, pathKey).
85 Scan(&previous.Scope, &previous.WorkspaceRoot, &previous.workspaceKey, &previous.TopicID); err == nil && previous.TopicID != "" {
86 affected[previous] = struct{}{}
87 } else if err != nil && !errors.Is(err, sql.ErrNoRows) {
88 _ = tx.Rollback()
89 return dirtyDirectories, err
90 }
91 generation := int64(0)
92 if generations != nil {
93 generation = generations[record.Path]
94 } else if cached, ok := directoryGenerations[directoryKey]; ok {
95 generation = cached
96 } else {
97 _ = tx.QueryRowContext(ctx, `SELECT scan_generation FROM catalog_directories WHERE path_key=?`, directoryKey).Scan(&generation)
98 directoryGenerations[directoryKey] = generation
99 }
100 if err := c.upsertSessionRow(ctx, tx, record, pathKey, directoryKey, generation, mode); err != nil {
101 _ = tx.Rollback()
102 return dirtyDirectories, err
103 }
104 if record.TopicID != "" {
105 affected[TopicKey{Scope: record.Scope, WorkspaceRoot: record.WorkspaceRoot,
106 workspaceKey: c.workspaceRootKey(record.Scope, record.WorkspaceRoot), TopicID: record.TopicID}] = struct{}{}
107 }
108 if err := c.updateFoldedTopicTombstones(ctx, tx, previous, record, c.opts.Now().UnixMilli()); err != nil {
109 _ = tx.Rollback()
110 return dirtyDirectories, err
111 }
112 roots[record.WorkspaceRoot] = struct{}{}
113 }
114 for key := range affected {
115 if err := c.recomputeTopic(ctx, tx, key); err != nil {
116 _ = tx.Rollback()
117 return dirtyDirectories, err
118 }
119 }
120 revision, err := bumpRevision(ctx, tx)
121 if err != nil {
122 _ = tx.Rollback()
123 return dirtyDirectories, err
124 }
125 if err := tx.Commit(); err != nil {
126 return dirtyDirectories, err
127 }
128 if notify {
129 c.publishRevision(revision, mapKeys(roots), reason)
130 } else {
131 c.rememberRevision(revision)
132 }
133 c.refreshCounts(ctx)
134 return dirtyDirectories, nil
135 }
136
137 const sessionInsertSQL = `INSERT INTO catalog_sessions(
138 path,path_key,directory,directory_key,scope,workspace_root,workspace_root_key,topic_id,topic_title,custom_title,
139 created_at,last_activity_at,preview,turns,turns_state,recovered,
140 recovery_reason,recovery_digest,parent_id,recovery_copy,recovery_group_id,
141 recovery_role,recovery_canonical,logical_topic_id,ordinary_visible,content_fingerprint,
142 meta_fingerprint,health,missing_since,seen_generation
143 ,repair_state,repair_attempts,repair_retry_at,repair_error_kind,repair_source_fingerprint,repair_engine_version
144 ,log_format,head_count,selected_head_id
145 ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
146 ON CONFLICT(path_key) DO UPDATE SET `
147
148 const repairScheduleUpdateSQL = `
149 repair_state=CASE
150 WHEN excluded.turns_state<>'unknown' THEN 'complete'
151 WHEN catalog_sessions.repair_source_fingerprint<>excluded.repair_source_fingerprint
152 OR catalog_sessions.repair_engine_version<>excluded.repair_engine_version THEN 'pending'
153 ELSE catalog_sessions.repair_state END,
154 repair_attempts=CASE
155 WHEN excluded.turns_state<>'unknown'
156 OR catalog_sessions.repair_source_fingerprint<>excluded.repair_source_fingerprint
157 OR catalog_sessions.repair_engine_version<>excluded.repair_engine_version THEN 0
158 ELSE catalog_sessions.repair_attempts END,
159 repair_retry_at=CASE
160 WHEN excluded.turns_state<>'unknown'
161 OR catalog_sessions.repair_source_fingerprint<>excluded.repair_source_fingerprint
162 OR catalog_sessions.repair_engine_version<>excluded.repair_engine_version THEN 0
163 ELSE catalog_sessions.repair_retry_at END,
164 repair_error_kind=CASE
165 WHEN excluded.turns_state<>'unknown'
166 OR catalog_sessions.repair_source_fingerprint<>excluded.repair_source_fingerprint
167 OR catalog_sessions.repair_engine_version<>excluded.repair_engine_version THEN ''
168 ELSE catalog_sessions.repair_error_kind END,
169 repair_source_fingerprint=excluded.repair_source_fingerprint,
170 repair_engine_version=excluded.repair_engine_version,
171 log_format=excluded.log_format, head_count=excluded.head_count, selected_head_id=excluded.selected_head_id`
172
173 const directoryProjectionUpdateSQL = `
174 path=excluded.path, directory=excluded.directory, directory_key=excluded.directory_key, scope=excluded.scope,
175 workspace_root=excluded.workspace_root, workspace_root_key=excluded.workspace_root_key, topic_id=excluded.topic_id,
176 topic_title=excluded.topic_title, custom_title=excluded.custom_title,
177 created_at=excluded.created_at, last_activity_at=excluded.last_activity_at,
178 preview=excluded.preview, turns=excluded.turns,
179 turns_state=excluded.turns_state, recovered=excluded.recovered,
180 recovery_reason=excluded.recovery_reason,
181 recovery_digest=excluded.recovery_digest, parent_id=excluded.parent_id,
182 recovery_copy=excluded.recovery_copy,
183 recovery_group_id=excluded.recovery_group_id,
184 recovery_role=excluded.recovery_role,
185 recovery_canonical=excluded.recovery_canonical,
186 logical_topic_id=excluded.logical_topic_id,
187 ordinary_visible=excluded.ordinary_visible,
188 content_fingerprint=excluded.content_fingerprint,
189 meta_fingerprint=excluded.meta_fingerprint, health=excluded.health,
190 missing_since=0, seen_generation=MAX(catalog_sessions.seen_generation, excluded.seen_generation),` + repairScheduleUpdateSQL
191
192 const exactSourceUpdateSQL = `
193 path=excluded.path, directory=excluded.directory, directory_key=excluded.directory_key, scope=excluded.scope,
194 workspace_root=excluded.workspace_root, workspace_root_key=excluded.workspace_root_key,
195 topic_id=CASE
196 WHEN catalog_sessions.recovered=1 OR excluded.recovered=1 OR catalog_sessions.recovery_group_id<>''
197 THEN catalog_sessions.topic_id ELSE excluded.topic_id END,
198 topic_title=CASE
199 WHEN catalog_sessions.recovered=1 OR excluded.recovered=1 OR catalog_sessions.recovery_group_id<>''
200 THEN catalog_sessions.topic_title ELSE excluded.topic_title END,
201 custom_title=excluded.custom_title,
202 created_at=excluded.created_at, last_activity_at=excluded.last_activity_at,
203 preview=excluded.preview, turns=excluded.turns,
204 turns_state=excluded.turns_state, recovered=excluded.recovered,
205 recovery_reason=excluded.recovery_reason,
206 recovery_digest=excluded.recovery_digest, parent_id=excluded.parent_id,
207 recovery_copy=catalog_sessions.recovery_copy,
208 recovery_group_id=catalog_sessions.recovery_group_id,
209 recovery_role=catalog_sessions.recovery_role,
210 recovery_canonical=catalog_sessions.recovery_canonical,
211 logical_topic_id=catalog_sessions.logical_topic_id,
212 ordinary_visible=catalog_sessions.ordinary_visible,
213 content_fingerprint=excluded.content_fingerprint,
214 meta_fingerprint=excluded.meta_fingerprint, health=excluded.health,
215 missing_since=0, seen_generation=MAX(catalog_sessions.seen_generation, excluded.seen_generation),` + repairScheduleUpdateSQL
216
217 func (c *Catalog) upsertSessionRow(ctx context.Context, tx *sql.Tx, record SessionRecord, pathKey, directoryKey string, generation int64, mode sessionUpsertMode) error {
218 updateSQL := directoryProjectionUpdateSQL
219 if mode == upsertExactSource {
220 updateSQL = exactSourceUpdateSQL
221 }
222 if _, err := tx.ExecContext(ctx, sessionInsertSQL+updateSQL, c.sessionRowValues(record, pathKey, directoryKey, generation)...); err != nil {
223 return err
224 }
225 return upsertHeadRows(ctx, tx, pathKey, record.heads)
226 }
227
228 func (c *Catalog) sessionRowValues(record SessionRecord, pathKey, directoryKey string, generation int64) []any {
229 repairState := "complete"
230 if record.TurnsState == TurnsUnknown {
231 repairState = "pending"
232 }
233 return []any{
234 record.Path, pathKey, record.Directory, directoryKey, record.Scope, record.WorkspaceRoot,
235 c.workspaceRootKey(record.Scope, record.WorkspaceRoot), record.TopicID, record.TopicTitle, record.CustomTitle, record.CreatedAt,
236 record.LastActivityAt, record.Preview, record.Turns, record.TurnsState,
237 record.Recovered, record.RecoveryReason, record.RecoveryDigest,
238 record.ParentID, boolToInt(record.RecoveryCopy), record.RecoveryGroupID,
239 record.RecoveryRole, boolToInt(record.RecoveryCanonical),
240 record.LogicalTopicID, boolToInt(record.OrdinaryVisible),
241 record.ContentFingerprint, record.MetaFingerprint,
242 record.Health, 0, generation,
243 repairState, 0, 0, "", repairSourceFingerprint(record), repairEngineVersion,
244 max(record.LogFormat, 1), record.HeadCount, record.SelectedHeadID,
245 }
246 }
247
248 func repairSourceFingerprint(record SessionRecord) string {
249 return record.ContentFingerprint + "\x00" + record.MetaFingerprint
250 }
251
251 lines GO