@morlay/session-rdb 0.0.21-alpha.2 → 0.0.21-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/artifact.d.mts +1 -1
- package/dist/artifact.mjs +1 -1
- package/dist/{backend-DpdtxYpz.d.mts → backend-a0xDrUFG.d.mts} +58 -9
- package/dist/{branch-5JzX9rUq.mjs → branch-Dlmbkfjj.mjs} +4 -12
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{log-CnYct2Dv.mjs → log-C-rbdFwK.mjs} +32 -4
- package/dist/{sqlite-fpvm5Dzs.mjs → sqlite-C3LHLzYW.mjs} +282 -23
- package/dist/{src-CWTWV7vx.mjs → src-Bn0038Sp.mjs} +269 -165
- package/dist/storage.d.mts +11 -2
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +11 -11
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +1 -1
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +1 -1
- package/package.json +21 -21
- package/src/backend.ts +26 -1
- package/src/branch.ts +5 -13
- package/src/entities/v3/event-usage.ts +2 -1
- package/src/index.ts +132 -33
- package/src/log.ts +36 -19
- package/src/postgres.ts +232 -38
- package/src/sqlite.ts +204 -25
- package/src/usage.ts +96 -32
package/src/postgres.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistenc
|
|
|
10
10
|
import {
|
|
11
11
|
type Backend,
|
|
12
12
|
type BackendTx,
|
|
13
|
+
type EventCountBucket,
|
|
13
14
|
type EventInsert,
|
|
14
15
|
type EventRow,
|
|
15
16
|
type SessionRow,
|
|
@@ -19,10 +20,34 @@ import { postgresTableDefs } from "./entities/index.ts";
|
|
|
19
20
|
import { sessionConflictRow, sessionInsertRow, titleOfEventData, usageRowOf } from "./log.ts";
|
|
20
21
|
import { createStorageRepository } from "./storage-takeover/repository.ts";
|
|
21
22
|
import type { StorageRepository } from "./storage-takeover/types.ts";
|
|
22
|
-
import
|
|
23
|
+
import {
|
|
24
|
+
COUNTED_EVENT_TYPES,
|
|
25
|
+
addActivityCount,
|
|
26
|
+
addTokenTotals,
|
|
27
|
+
addTotals,
|
|
28
|
+
emptyTotals,
|
|
29
|
+
localDayKey,
|
|
30
|
+
} from "./usage.ts";
|
|
31
|
+
import type { UsageActivityTotals, UsageAggregate, UsageTokenTotals } from "./usage.ts";
|
|
23
32
|
|
|
24
33
|
const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
|
|
25
34
|
|
|
35
|
+
const pgWriteQueues = new Map<string, Promise<void>>();
|
|
36
|
+
|
|
37
|
+
/** 按介质(连接串 + schema)串行化写事务,与 SQLite 侧的 `enqueueSqliteTx` 同形。 */
|
|
38
|
+
function enqueuePgWrite<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
39
|
+
const tail = pgWriteQueues.get(key) ?? Promise.resolve();
|
|
40
|
+
const run = tail.then(fn);
|
|
41
|
+
pgWriteQueues.set(
|
|
42
|
+
key,
|
|
43
|
+
run.then(
|
|
44
|
+
() => undefined,
|
|
45
|
+
() => undefined,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
return run;
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
/** pg 的 text 列窄化:非字符串(含 null)都不当作文本。 */
|
|
27
52
|
function text(value: unknown): string | null {
|
|
28
53
|
return typeof value === "string" ? value : null;
|
|
@@ -35,9 +60,8 @@ function numeric(value: unknown): number {
|
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
/** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
|
|
38
|
-
function
|
|
63
|
+
function tokenTotalsOf(row: Record<string, unknown>): UsageTokenTotals {
|
|
39
64
|
return {
|
|
40
|
-
events: numeric(row["events"]),
|
|
41
65
|
inputTokens: numeric(row["input_tokens"]),
|
|
42
66
|
outputTokens: numeric(row["output_tokens"]),
|
|
43
67
|
cacheReadTokens: numeric(row["cache_read_tokens"]),
|
|
@@ -46,6 +70,13 @@ function totalsOfRow(row: Record<string, unknown>): UsageTotals {
|
|
|
46
70
|
};
|
|
47
71
|
}
|
|
48
72
|
|
|
73
|
+
function emptyActivity(): UsageActivityTotals {
|
|
74
|
+
return { turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
78
|
+
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
79
|
+
|
|
49
80
|
export interface PostgresBackendOptions {
|
|
50
81
|
identityBase: string;
|
|
51
82
|
|
|
@@ -79,21 +110,28 @@ export class PostgresBackend implements Backend {
|
|
|
79
110
|
});
|
|
80
111
|
|
|
81
112
|
this.opened.catch(() => {});
|
|
113
|
+
// 写事务按介质串行(与 SQLite 侧的 enqueueSqliteTx 同形):并发 `writeAtomically`
|
|
114
|
+
// 会互相覆盖实例级的 `txOverride`,让先开始的事务的语句落到别人的事务(一次失败
|
|
115
|
+
// 会把另一个成功事务一起拖垮),或退化成 autocommit。介质身份用 `identityBase`
|
|
116
|
+
// (宿主 + 库 + schema)——同一进程内多实例连同一库时也共用同一条队列。
|
|
117
|
+
const writeQueueKey = options.identityBase;
|
|
82
118
|
this.storage = createStorageRepository({
|
|
83
119
|
db: () => this.opened.then(() => this.txOverride ?? this.db),
|
|
84
120
|
|
|
85
121
|
writeAtomically: (fn) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
enqueuePgWrite(writeQueueKey, () =>
|
|
123
|
+
this.db.transaction(
|
|
124
|
+
async (tx) => {
|
|
125
|
+
const previous = this.txOverride;
|
|
126
|
+
this.txOverride = tx;
|
|
127
|
+
try {
|
|
128
|
+
return await fn();
|
|
129
|
+
} finally {
|
|
130
|
+
this.txOverride = previous;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{ isolationLevel: "serializable" },
|
|
134
|
+
),
|
|
97
135
|
),
|
|
98
136
|
tables: this.tables,
|
|
99
137
|
});
|
|
@@ -124,6 +162,8 @@ export class PostgresBackend implements Backend {
|
|
|
124
162
|
}
|
|
125
163
|
}
|
|
126
164
|
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
165
|
+
await this.ensureEventCountsTable();
|
|
166
|
+
await this.backfillEventCounts();
|
|
127
167
|
await this.backfillEventUsage();
|
|
128
168
|
const storeId = await this.db.transaction(async (tx) => {
|
|
129
169
|
await tx
|
|
@@ -194,9 +234,13 @@ export class PostgresBackend implements Backend {
|
|
|
194
234
|
.execute() as unknown as EventRow[];
|
|
195
235
|
}
|
|
196
236
|
|
|
237
|
+
// 只取类型:rewind 的边界 / 窗口探测不该把整个事件 JSON(f_data)拖回来。
|
|
197
238
|
async getEventTypeAt(id: SessionId, sequence: number): Promise<string | undefined> {
|
|
198
239
|
const table = this.tables["t_session_events"];
|
|
199
|
-
const rows = (await this.
|
|
240
|
+
const rows = (await this.db
|
|
241
|
+
.select({ fType: this.tables["t_events"].fType })
|
|
242
|
+
.from(table)
|
|
243
|
+
.innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId))
|
|
200
244
|
.where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence)))
|
|
201
245
|
.limit(1)
|
|
202
246
|
.execute()) as Array<{ fType?: string }>;
|
|
@@ -209,7 +253,10 @@ export class PostgresBackend implements Backend {
|
|
|
209
253
|
limit: number,
|
|
210
254
|
): Promise<Array<Pick<EventRow, "fSequence" | "fType">>> {
|
|
211
255
|
const table = this.tables["t_session_events"];
|
|
212
|
-
return (await this.
|
|
256
|
+
return (await this.db
|
|
257
|
+
.select({ fSequence: table.fSequence, fType: this.tables["t_events"].fType })
|
|
258
|
+
.from(table)
|
|
259
|
+
.innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId))
|
|
213
260
|
.where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence)))
|
|
214
261
|
.orderBy(desc(table.fSequence))
|
|
215
262
|
.limit(limit)
|
|
@@ -552,62 +599,209 @@ export class PostgresBackend implements Backend {
|
|
|
552
599
|
|
|
553
600
|
/** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
|
|
554
601
|
async usageReport(sinceMs?: number): Promise<UsageAggregate> {
|
|
555
|
-
const
|
|
602
|
+
const usageSince = sinceMs === undefined ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
|
|
603
|
+
// 活动计数走派生表的「本地日」列(与桶的本地日口径一致)。
|
|
604
|
+
const daySince = sinceMs === undefined ? sql`` : sql` AND c.f_day >= ${localDayKey(sinceMs)}`;
|
|
556
605
|
const tEventUsage = this.tables["t_event_usage"];
|
|
557
606
|
const tSessionEvents = this.tables["t_session_events"];
|
|
558
607
|
const tSessions = this.tables["t_sessions"];
|
|
608
|
+
const eventCountsTable = this.qualifiedTable("t_event_counts");
|
|
609
|
+
const subagentOfUsage = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
610
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
611
|
+
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent')`;
|
|
612
|
+
const subagentOfCounts = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
613
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
614
|
+
WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
|
|
615
|
+
// 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致。
|
|
616
|
+
const hasUsageSession = (
|
|
617
|
+
column: string,
|
|
618
|
+
): ReturnType<typeof sql> => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
|
|
619
|
+
JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
|
|
620
|
+
WHERE ub.f_session_id = ${sql.raw(column)})`;
|
|
621
|
+
const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
|
|
622
|
+
sum(u.f_output_tokens) AS output_tokens,
|
|
623
|
+
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
624
|
+
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
625
|
+
sum(u.f_total_tokens) AS total_tokens`);
|
|
626
|
+
|
|
559
627
|
const buckets = (await this.db.execute(sql`
|
|
560
628
|
SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
|
|
561
629
|
u.f_provider AS provider,
|
|
562
630
|
u.f_model AS model,
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
|
|
566
|
-
count(*) AS events,
|
|
567
|
-
sum(u.f_input_tokens) AS input_tokens,
|
|
568
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
569
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
570
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
571
|
-
sum(u.f_total_tokens) AS total_tokens
|
|
631
|
+
${subagentOfUsage} AS subagent,
|
|
632
|
+
${tokenColumns}
|
|
572
633
|
FROM ${tEventUsage} u
|
|
573
|
-
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${
|
|
634
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
574
635
|
GROUP BY 1, 2, 3, 4
|
|
575
636
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
576
|
-
const
|
|
637
|
+
const tokenScope = (await this.db.execute(sql`
|
|
638
|
+
SELECT ${subagentOfUsage} AS subagent,
|
|
639
|
+
${tokenColumns}
|
|
640
|
+
FROM ${tEventUsage} u
|
|
641
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
642
|
+
GROUP BY 1
|
|
643
|
+
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
644
|
+
const activityScope = (await this.db.execute(sql`
|
|
645
|
+
SELECT ${subagentOfCounts} AS subagent,
|
|
646
|
+
c.f_type AS f_type,
|
|
647
|
+
sum(c.f_count) AS n
|
|
648
|
+
FROM ${sql.raw(eventCountsTable)} c
|
|
649
|
+
JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
|
|
650
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
651
|
+
GROUP BY 1, 2
|
|
652
|
+
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
653
|
+
const tokenSessions = (await this.db.execute(sql`
|
|
577
654
|
SELECT b.f_session_id AS session_id,
|
|
578
655
|
s.f_title AS title,
|
|
579
656
|
(s.f_origin = 'subagent') AS subagent,
|
|
580
657
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
581
|
-
|
|
582
|
-
sum(u.f_input_tokens) AS input_tokens,
|
|
583
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
584
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
585
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
586
|
-
sum(u.f_total_tokens) AS total_tokens
|
|
658
|
+
${tokenColumns}
|
|
587
659
|
FROM ${tSessionEvents} b
|
|
588
660
|
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
589
661
|
JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
|
|
590
|
-
WHERE 1 = 1${
|
|
662
|
+
WHERE 1 = 1${usageSince}
|
|
591
663
|
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
592
664
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
665
|
+
const activitySessions = (await this.db.execute(sql`
|
|
666
|
+
SELECT c.f_session_id AS session_id,
|
|
667
|
+
c.f_type AS f_type,
|
|
668
|
+
sum(c.f_count) AS n
|
|
669
|
+
FROM ${sql.raw(eventCountsTable)} c
|
|
670
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
671
|
+
GROUP BY c.f_session_id, c.f_type
|
|
672
|
+
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
673
|
+
const human = emptyTotals();
|
|
674
|
+
const subagent = emptyTotals();
|
|
675
|
+
for (const row of tokenScope.rows) {
|
|
676
|
+
addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
|
|
677
|
+
}
|
|
678
|
+
for (const row of activityScope.rows) {
|
|
679
|
+
addActivityCount(
|
|
680
|
+
row["subagent"] === true ? subagent : human,
|
|
681
|
+
String(row["f_type"]),
|
|
682
|
+
numeric(row["n"]),
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
686
|
+
|
|
687
|
+
const activityBySession = new Map<string, UsageActivityTotals>();
|
|
688
|
+
for (const row of activitySessions.rows) {
|
|
689
|
+
const key = String(row["session_id"]);
|
|
690
|
+
const target = activityBySession.get(key) ?? emptyActivity();
|
|
691
|
+
addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
|
|
692
|
+
activityBySession.set(key, target);
|
|
693
|
+
}
|
|
593
694
|
return {
|
|
695
|
+
totals,
|
|
696
|
+
subagent,
|
|
697
|
+
human,
|
|
594
698
|
buckets: buckets.rows.map((row) => ({
|
|
595
699
|
day: String(row["day"]),
|
|
596
700
|
provider: text(row["provider"]),
|
|
597
701
|
model: text(row["model"]),
|
|
598
702
|
subagent: row["subagent"] === true,
|
|
599
|
-
...
|
|
703
|
+
...tokenTotalsOf(row),
|
|
600
704
|
})),
|
|
601
|
-
sessions:
|
|
705
|
+
sessions: tokenSessions.rows.map((row) => ({
|
|
602
706
|
sessionId: String(row["session_id"]),
|
|
603
707
|
title: text(row["title"]),
|
|
604
708
|
subagent: row["subagent"] === true,
|
|
605
709
|
archived: row["archived"] === true,
|
|
606
|
-
...
|
|
710
|
+
...tokenTotalsOf(row),
|
|
711
|
+
...(activityBySession.get(String(row["session_id"])) ?? emptyActivity()),
|
|
607
712
|
})),
|
|
608
713
|
};
|
|
609
714
|
}
|
|
610
715
|
|
|
716
|
+
/** 表名限定到配置的 schema(派生表不进 drizzle schema / 迁移,建表幂等)。 */
|
|
717
|
+
private qualifiedTable(name: string): string {
|
|
718
|
+
const schema = this.options.schema ?? "public";
|
|
719
|
+
return schema === "public" ? name : `"${schema}".${name}`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private async ensureEventCountsTable(): Promise<void> {
|
|
723
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
724
|
+
await this.db.execute(
|
|
725
|
+
sql.raw(`CREATE TABLE IF NOT EXISTS ${table} (
|
|
726
|
+
f_id serial PRIMARY KEY,
|
|
727
|
+
f_session_id text NOT NULL,
|
|
728
|
+
f_day text NOT NULL,
|
|
729
|
+
f_type text NOT NULL,
|
|
730
|
+
f_count integer NOT NULL DEFAULT 0
|
|
731
|
+
)`),
|
|
732
|
+
);
|
|
733
|
+
await this.db.execute(
|
|
734
|
+
sql.raw(
|
|
735
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS uq_event_counts_session_day_type ON ${table}(f_session_id, f_day, f_type)`,
|
|
736
|
+
),
|
|
737
|
+
);
|
|
738
|
+
await this.db.execute(
|
|
739
|
+
sql.raw(`CREATE INDEX IF NOT EXISTS idx_event_counts_day ON ${table}(f_day)`),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
|
|
744
|
+
private async backfillEventCounts(): Promise<void> {
|
|
745
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
746
|
+
const existing = (await this.db.execute(
|
|
747
|
+
sql.raw(`SELECT count(*) AS n FROM ${table}`),
|
|
748
|
+
)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
749
|
+
if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
|
|
750
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
751
|
+
const tEvents = this.tables["t_events"];
|
|
752
|
+
await this.db.execute(sql`
|
|
753
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
754
|
+
SELECT ${tSessionEvents}.f_session_id,
|
|
755
|
+
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
756
|
+
${tEvents}.f_type,
|
|
757
|
+
count(*)
|
|
758
|
+
FROM ${tSessionEvents}
|
|
759
|
+
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
760
|
+
WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
761
|
+
GROUP BY 1, 2, 3
|
|
762
|
+
ON CONFLICT DO NOTHING
|
|
763
|
+
`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** 活动计数旁路累加:派生表,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
767
|
+
async incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void> {
|
|
768
|
+
if (buckets.length === 0) return;
|
|
769
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
770
|
+
for (const bucket of buckets) {
|
|
771
|
+
await this.db.execute(sql`
|
|
772
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
773
|
+
VALUES (${id}, ${bucket.day}, ${bucket.type}, ${bucket.count})
|
|
774
|
+
ON CONFLICT (f_session_id, f_day, f_type)
|
|
775
|
+
DO UPDATE SET f_count = ${sql.raw(`${table}.f_count`)} + EXCLUDED.f_count
|
|
776
|
+
`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
781
|
+
async rebuildEventCounts(id: SessionId): Promise<void> {
|
|
782
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
783
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
784
|
+
const tEvents = this.tables["t_events"];
|
|
785
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
|
|
786
|
+
await this.db.execute(sql`
|
|
787
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
788
|
+
SELECT ${tSessionEvents}.f_session_id,
|
|
789
|
+
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
790
|
+
${tEvents}.f_type,
|
|
791
|
+
count(*)
|
|
792
|
+
FROM ${tSessionEvents}
|
|
793
|
+
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
794
|
+
WHERE ${tSessionEvents}.f_session_id = ${id}
|
|
795
|
+
AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
796
|
+
GROUP BY 1, 2, 3
|
|
797
|
+
`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async deleteEventCounts(id: SessionId): Promise<void> {
|
|
801
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
802
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
|
|
803
|
+
}
|
|
804
|
+
|
|
611
805
|
private async getPrevBridge(
|
|
612
806
|
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
613
807
|
id: SessionId,
|