@hyperfixation/db 0.1.0
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/LICENSE +21 -0
- package/dist/app-state.d.ts +12 -0
- package/dist/app-state.js +18 -0
- package/dist/boot-checks.d.ts +56 -0
- package/dist/boot-checks.js +228 -0
- package/dist/classify.d.ts +6 -0
- package/dist/classify.js +75 -0
- package/dist/control-plane.d.ts +75 -0
- package/dist/control-plane.js +155 -0
- package/dist/delete-guard.d.ts +32 -0
- package/dist/delete-guard.js +62 -0
- package/dist/fenced-client.d.ts +35 -0
- package/dist/fenced-client.js +153 -0
- package/dist/grant-ro.d.ts +23 -0
- package/dist/grant-ro.js +49 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/internal/control-pool.d.ts +20 -0
- package/dist/internal/control-pool.js +17 -0
- package/dist/loader.d.ts +15 -0
- package/dist/loader.js +82 -0
- package/dist/migrate.d.ts +54 -0
- package/dist/migrate.js +143 -0
- package/dist/migration-policy.d.ts +14 -0
- package/dist/migration-policy.js +85 -0
- package/dist/migrator.d.ts +9 -0
- package/dist/migrator.js +9 -0
- package/dist/roles.d.ts +47 -0
- package/dist/roles.js +112 -0
- package/dist/schema/app.d.ts +235 -0
- package/dist/schema/app.js +23 -0
- package/dist/schema/approvals.d.ts +352 -0
- package/dist/schema/approvals.js +43 -0
- package/dist/schema/auth.d.ts +1272 -0
- package/dist/schema/auth.js +120 -0
- package/dist/schema/index.d.ts +7 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/ledger.d.ts +722 -0
- package/dist/schema/ledger.js +68 -0
- package/dist/schema/machinery.d.ts +1343 -0
- package/dist/schema/machinery.js +146 -0
- package/dist/schema/records.d.ts +15 -0
- package/dist/schema/records.js +16 -0
- package/dist/schema/runs.d.ts +213 -0
- package/dist/schema/runs.js +26 -0
- package/dist/step-pool.d.ts +26 -0
- package/dist/step-pool.js +57 -0
- package/migrations/0000_core_schema.sql +185 -0
- package/migrations/0001_llm_call_reservation_index.sql +4 -0
- package/migrations/0002_approvals.sql +25 -0
- package/migrations/0003_auth_invitation.sql +13 -0
- package/migrations/0004_machinery.sql +105 -0
- package/migrations/0005_nullable_activity_task_record.sql +4 -0
- package/migrations/0006_activity_score_key.sql +5 -0
- package/migrations/0007_score_spec_name.sql +3 -0
- package/migrations/meta/0000_snapshot.json +1238 -0
- package/migrations/meta/0001_snapshot.json +1238 -0
- package/migrations/meta/0002_snapshot.json +1426 -0
- package/migrations/meta/0003_snapshot.json +1516 -0
- package/migrations/meta/0004_snapshot.json +2353 -0
- package/migrations/meta/0005_snapshot.json +2353 -0
- package/migrations/meta/0006_snapshot.json +2415 -0
- package/migrations/meta/0007_snapshot.json +2427 -0
- package/migrations/meta/_journal.json +62 -0
- package/package.json +58 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { bigint, doublePrecision, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core";
|
|
3
|
+
// None of these tables carries a foreign key to an app table (E004 applies the other way
|
|
4
|
+
// round, but the core cannot know an app's tables either), and every `record_id` is `text`
|
|
5
|
+
// — a record id is carried, not joined on, matching hf_approval and hf_action_log.
|
|
6
|
+
// Lock order: all of them sit in the last tier, with the app's own tables.
|
|
7
|
+
export const sourceRunStatuses = ["running", "ok", "error"];
|
|
8
|
+
export const sourceRecordStatuses = ["new", "linked", "review", "error"];
|
|
9
|
+
// `created` is the link for a record the resolver made for this source row; the column is
|
|
10
|
+
// plain `text`, so widening the union is a TypeScript change with no migration behind it.
|
|
11
|
+
export const recordLinkMethods = ["exact", "fuzzy", "created", "manual", "human_confirmed"];
|
|
12
|
+
export const taskOrigins = ["flow", "manual", "sweep"];
|
|
13
|
+
export const labelTargets = ["score", "draft", "record"];
|
|
14
|
+
export const labelValues = ["up", "down", "correction"];
|
|
15
|
+
const at = (name) => timestamp(name, { withTimezone: true, mode: "date" });
|
|
16
|
+
const id = () => bigint("id", { mode: "number" }).generatedAlwaysAsIdentity().primaryKey();
|
|
17
|
+
export const hfSourceRun = pgTable("hf_source_run", {
|
|
18
|
+
id: id(),
|
|
19
|
+
source: text("source").notNull(),
|
|
20
|
+
startedAt: at("started_at").notNull().defaultNow(),
|
|
21
|
+
finishedAt: at("finished_at"),
|
|
22
|
+
status: text("status", { enum: sourceRunStatuses }).notNull(),
|
|
23
|
+
rowsIn: integer("rows_in").notNull().default(0),
|
|
24
|
+
rowsNew: integer("rows_new").notNull().default(0),
|
|
25
|
+
rowsChanged: integer("rows_changed").notNull().default(0),
|
|
26
|
+
error: text("error"),
|
|
27
|
+
});
|
|
28
|
+
export const hfSourceRecord = pgTable("hf_source_record", {
|
|
29
|
+
id: id(),
|
|
30
|
+
source: text("source").notNull(),
|
|
31
|
+
externalId: text("external_id").notNull(),
|
|
32
|
+
payload: jsonb("payload").notNull(),
|
|
33
|
+
payloadHash: text("payload_hash").notNull(),
|
|
34
|
+
status: text("status", { enum: sourceRecordStatuses }).notNull().default("new"),
|
|
35
|
+
attempts: integer("attempts").notNull().default(0),
|
|
36
|
+
error: text("error"),
|
|
37
|
+
runId: bigint("run_id", { mode: "number" }),
|
|
38
|
+
firstSeen: at("first_seen").notNull().defaultNow(),
|
|
39
|
+
lastSeen: at("last_seen").notNull().defaultNow(),
|
|
40
|
+
}, (t) => [
|
|
41
|
+
uniqueIndex("hf_source_record_source_external_uq").on(t.source, t.externalId),
|
|
42
|
+
// The resolver's batch scan: what is still `new` or waiting in `review`.
|
|
43
|
+
index("hf_source_record_status_idx").on(t.status),
|
|
44
|
+
]);
|
|
45
|
+
export const hfRecordLink = pgTable("hf_record_link", {
|
|
46
|
+
id: id(),
|
|
47
|
+
sourceRecordId: bigint("source_record_id", { mode: "number" }).notNull(),
|
|
48
|
+
recordType: text("record_type").notNull(),
|
|
49
|
+
recordId: text("record_id").notNull(),
|
|
50
|
+
confidence: doublePrecision("confidence"),
|
|
51
|
+
method: text("method", { enum: recordLinkMethods }).notNull(),
|
|
52
|
+
decidedBy: text("decided_by"),
|
|
53
|
+
decidedAt: at("decided_at"),
|
|
54
|
+
}, (t) => [
|
|
55
|
+
uniqueIndex("hf_record_link_source_record_uq").on(t.sourceRecordId),
|
|
56
|
+
index("hf_record_link_record_idx").on(t.recordType, t.recordId),
|
|
57
|
+
]);
|
|
58
|
+
export const hfScore = pgTable("hf_score", {
|
|
59
|
+
id: id(),
|
|
60
|
+
recordType: text("record_type").notNull(),
|
|
61
|
+
recordId: text("record_id").notNull(),
|
|
62
|
+
// Which spec decided this, alongside the version it decided under: two specs score one
|
|
63
|
+
// record, and without the name their rows are indistinguishable. Nullable for the rows
|
|
64
|
+
// written before it existed — every write path fills it.
|
|
65
|
+
specName: text("spec_name"),
|
|
66
|
+
specVersion: integer("spec_version").notNull(),
|
|
67
|
+
score: doublePrecision("score").notNull(),
|
|
68
|
+
explanation: text("explanation"),
|
|
69
|
+
// The ledger row of the call that produced an LLM-assigned score; a rule-based score has none.
|
|
70
|
+
llmCallId: bigint("llm_call_id", { mode: "number" }),
|
|
71
|
+
// What makes a step-side score write replay-safe: attempt 2 runs the step again under a new
|
|
72
|
+
// workflow id, and `(run_id, key, spec_name)` is what it conflicts on. Null for a web-side
|
|
73
|
+
// write. The spec name is in the key because one step may score a record under two specs,
|
|
74
|
+
// which share the step's key and must not collapse into one row.
|
|
75
|
+
runId: text("run_id"),
|
|
76
|
+
key: text("key"),
|
|
77
|
+
createdAt: at("created_at").notNull().defaultNow(),
|
|
78
|
+
}, (t) => [
|
|
79
|
+
index("hf_score_record_idx").on(t.recordType, t.recordId),
|
|
80
|
+
uniqueIndex("hf_score_run_key_spec_uq")
|
|
81
|
+
.on(t.runId, t.key, t.specName)
|
|
82
|
+
.where(sql `${t.key} IS NOT NULL`),
|
|
83
|
+
]);
|
|
84
|
+
export const hfActivity = pgTable("hf_activity", {
|
|
85
|
+
id: id(),
|
|
86
|
+
// Nullable, like `hf_approval`'s and `hf_action_log`'s: a row about no record writes NULL,
|
|
87
|
+
// which E002 ignores. A stand-in such as `'hf_approval'` would fail it at the next boot.
|
|
88
|
+
recordType: text("record_type"),
|
|
89
|
+
recordId: text("record_id"),
|
|
90
|
+
kind: text("kind").notNull(),
|
|
91
|
+
actorId: text("actor_id"),
|
|
92
|
+
body: text("body"),
|
|
93
|
+
meta: jsonb("meta"),
|
|
94
|
+
// The run that wrote it, for the timeline; null for a web-side write (a label, an
|
|
95
|
+
// outcome, a manual task), which the timeline groups under "manual".
|
|
96
|
+
runId: text("run_id"),
|
|
97
|
+
// The idempotency key of the step that wrote it; null for a web-side write, which happens
|
|
98
|
+
// once per request and has no replay to survive.
|
|
99
|
+
key: text("key"),
|
|
100
|
+
at: at("at").notNull().defaultNow(),
|
|
101
|
+
}, (t) => [
|
|
102
|
+
index("hf_activity_record_idx").on(t.recordType, t.recordId),
|
|
103
|
+
uniqueIndex("hf_activity_run_key_uq")
|
|
104
|
+
.on(t.runId, t.key)
|
|
105
|
+
.where(sql `${t.key} IS NOT NULL`),
|
|
106
|
+
]);
|
|
107
|
+
export const hfTask = pgTable("hf_task", {
|
|
108
|
+
id: id(),
|
|
109
|
+
// Nullable for the same reason as `hf_activity`'s above.
|
|
110
|
+
recordType: text("record_type"),
|
|
111
|
+
recordId: text("record_id"),
|
|
112
|
+
title: text("title").notNull(),
|
|
113
|
+
dueAt: at("due_at"),
|
|
114
|
+
ownerId: text("owner_id"),
|
|
115
|
+
doneAt: at("done_at"),
|
|
116
|
+
cancelledAt: at("cancelled_at"),
|
|
117
|
+
origin: text("origin", { enum: taskOrigins }).notNull(),
|
|
118
|
+
// Makes "one task per uncertain action row, once" an `ON CONFLICT` target for
|
|
119
|
+
// `reconcile()` and `actions.perform`; set to the action-log row's id.
|
|
120
|
+
originRef: text("origin_ref"),
|
|
121
|
+
createdAt: at("created_at").notNull().defaultNow(),
|
|
122
|
+
}, (t) => [
|
|
123
|
+
index("hf_task_record_idx").on(t.recordType, t.recordId),
|
|
124
|
+
uniqueIndex("hf_task_origin_ref_uq")
|
|
125
|
+
.on(t.origin, t.originRef)
|
|
126
|
+
.where(sql `${t.originRef} IS NOT NULL`),
|
|
127
|
+
]);
|
|
128
|
+
export const hfLabel = pgTable("hf_label", {
|
|
129
|
+
id: id(),
|
|
130
|
+
recordType: text("record_type").notNull(),
|
|
131
|
+
recordId: text("record_id").notNull(),
|
|
132
|
+
target: text("target", { enum: labelTargets }).notNull(),
|
|
133
|
+
targetId: text("target_id"),
|
|
134
|
+
value: text("value", { enum: labelValues }).notNull(),
|
|
135
|
+
correction: jsonb("correction"),
|
|
136
|
+
userId: text("user_id"),
|
|
137
|
+
createdAt: at("created_at").notNull().defaultNow(),
|
|
138
|
+
}, (t) => [index("hf_label_record_idx").on(t.recordType, t.recordId)]);
|
|
139
|
+
export const hfOutcome = pgTable("hf_outcome", {
|
|
140
|
+
id: id(),
|
|
141
|
+
recordType: text("record_type").notNull(),
|
|
142
|
+
recordId: text("record_id").notNull(),
|
|
143
|
+
outcome: text("outcome").notNull(),
|
|
144
|
+
at: at("at").notNull().defaultNow(),
|
|
145
|
+
notes: text("notes"),
|
|
146
|
+
}, (t) => [index("hf_outcome_record_idx").on(t.recordType, t.recordId)]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spread into an app's record table. Every column is nullable or DB-defaulted and none is
|
|
3
|
+
* unique, so adding the mixin to an existing table is an allowed app migration. The trigram
|
|
4
|
+
* index on `normalized_name` is the app's to declare (E003 fails boot without it).
|
|
5
|
+
*/
|
|
6
|
+
export declare const hfRecordColumns: () => {
|
|
7
|
+
createdAt: import("drizzle-orm").HasDefault<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"created_at">>;
|
|
8
|
+
updatedAt: import("drizzle-orm").HasDefault<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"updated_at">>;
|
|
9
|
+
archivedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"archived_at">;
|
|
10
|
+
stage: import("drizzle-orm/pg-core").PgTextBuilderInitial<"stage", [string, ...string[]]>;
|
|
11
|
+
score: import("drizzle-orm/pg-core").PgDoublePrecisionBuilderInitial<"score">;
|
|
12
|
+
scoreExplanation: import("drizzle-orm/pg-core").PgTextBuilderInitial<"score_explanation", [string, ...string[]]>;
|
|
13
|
+
specVersion: import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"spec_version">;
|
|
14
|
+
normalizedName: import("drizzle-orm/pg-core").PgTextBuilderInitial<"normalized_name", [string, ...string[]]>;
|
|
15
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { doublePrecision, integer, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
/**
|
|
3
|
+
* Spread into an app's record table. Every column is nullable or DB-defaulted and none is
|
|
4
|
+
* unique, so adding the mixin to an existing table is an allowed app migration. The trigram
|
|
5
|
+
* index on `normalized_name` is the app's to declare (E003 fails boot without it).
|
|
6
|
+
*/
|
|
7
|
+
export const hfRecordColumns = () => ({
|
|
8
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow(),
|
|
9
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow(),
|
|
10
|
+
archivedAt: timestamp("archived_at", { withTimezone: true, mode: "date" }),
|
|
11
|
+
stage: text("stage"),
|
|
12
|
+
score: doublePrecision("score"),
|
|
13
|
+
scoreExplanation: text("score_explanation"),
|
|
14
|
+
specVersion: integer("spec_version"),
|
|
15
|
+
normalizedName: text("normalized_name"),
|
|
16
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export declare const runStatuses: readonly ["running", "waiting", "paused", "done", "failed"];
|
|
2
|
+
export type RunStatus = (typeof runStatuses)[number];
|
|
3
|
+
export declare const hfRun: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
4
|
+
name: "hf_run";
|
|
5
|
+
schema: undefined;
|
|
6
|
+
columns: {
|
|
7
|
+
runId: import("drizzle-orm/pg-core").PgColumn<{
|
|
8
|
+
name: "run_id";
|
|
9
|
+
tableName: "hf_run";
|
|
10
|
+
dataType: "string";
|
|
11
|
+
columnType: "PgText";
|
|
12
|
+
data: string;
|
|
13
|
+
driverParam: string;
|
|
14
|
+
notNull: true;
|
|
15
|
+
hasDefault: false;
|
|
16
|
+
isPrimaryKey: true;
|
|
17
|
+
isAutoincrement: false;
|
|
18
|
+
hasRuntimeDefault: false;
|
|
19
|
+
enumValues: [string, ...string[]];
|
|
20
|
+
baseColumn: never;
|
|
21
|
+
identity: undefined;
|
|
22
|
+
generated: undefined;
|
|
23
|
+
}, {}, {}>;
|
|
24
|
+
flow: import("drizzle-orm/pg-core").PgColumn<{
|
|
25
|
+
name: "flow";
|
|
26
|
+
tableName: "hf_run";
|
|
27
|
+
dataType: "string";
|
|
28
|
+
columnType: "PgText";
|
|
29
|
+
data: string;
|
|
30
|
+
driverParam: string;
|
|
31
|
+
notNull: true;
|
|
32
|
+
hasDefault: false;
|
|
33
|
+
isPrimaryKey: false;
|
|
34
|
+
isAutoincrement: false;
|
|
35
|
+
hasRuntimeDefault: false;
|
|
36
|
+
enumValues: [string, ...string[]];
|
|
37
|
+
baseColumn: never;
|
|
38
|
+
identity: undefined;
|
|
39
|
+
generated: undefined;
|
|
40
|
+
}, {}, {}>;
|
|
41
|
+
input: import("drizzle-orm/pg-core").PgColumn<{
|
|
42
|
+
name: "input";
|
|
43
|
+
tableName: "hf_run";
|
|
44
|
+
dataType: "json";
|
|
45
|
+
columnType: "PgJsonb";
|
|
46
|
+
data: unknown;
|
|
47
|
+
driverParam: unknown;
|
|
48
|
+
notNull: false;
|
|
49
|
+
hasDefault: false;
|
|
50
|
+
isPrimaryKey: false;
|
|
51
|
+
isAutoincrement: false;
|
|
52
|
+
hasRuntimeDefault: false;
|
|
53
|
+
enumValues: undefined;
|
|
54
|
+
baseColumn: never;
|
|
55
|
+
identity: undefined;
|
|
56
|
+
generated: undefined;
|
|
57
|
+
}, {}, {}>;
|
|
58
|
+
status: import("drizzle-orm/pg-core").PgColumn<{
|
|
59
|
+
name: "status";
|
|
60
|
+
tableName: "hf_run";
|
|
61
|
+
dataType: "string";
|
|
62
|
+
columnType: "PgText";
|
|
63
|
+
data: "paused" | "failed" | "running" | "waiting" | "done";
|
|
64
|
+
driverParam: string;
|
|
65
|
+
notNull: true;
|
|
66
|
+
hasDefault: false;
|
|
67
|
+
isPrimaryKey: false;
|
|
68
|
+
isAutoincrement: false;
|
|
69
|
+
hasRuntimeDefault: false;
|
|
70
|
+
enumValues: ["running", "waiting", "paused", "done", "failed"];
|
|
71
|
+
baseColumn: never;
|
|
72
|
+
identity: undefined;
|
|
73
|
+
generated: undefined;
|
|
74
|
+
}, {}, {}>;
|
|
75
|
+
attempt: import("drizzle-orm/pg-core").PgColumn<{
|
|
76
|
+
name: "attempt";
|
|
77
|
+
tableName: "hf_run";
|
|
78
|
+
dataType: "number";
|
|
79
|
+
columnType: "PgInteger";
|
|
80
|
+
data: number;
|
|
81
|
+
driverParam: string | number;
|
|
82
|
+
notNull: true;
|
|
83
|
+
hasDefault: true;
|
|
84
|
+
isPrimaryKey: false;
|
|
85
|
+
isAutoincrement: false;
|
|
86
|
+
hasRuntimeDefault: false;
|
|
87
|
+
enumValues: undefined;
|
|
88
|
+
baseColumn: never;
|
|
89
|
+
identity: undefined;
|
|
90
|
+
generated: undefined;
|
|
91
|
+
}, {}, {}>;
|
|
92
|
+
currentWorkflowId: import("drizzle-orm/pg-core").PgColumn<{
|
|
93
|
+
name: "current_workflow_id";
|
|
94
|
+
tableName: "hf_run";
|
|
95
|
+
dataType: "string";
|
|
96
|
+
columnType: "PgText";
|
|
97
|
+
data: string;
|
|
98
|
+
driverParam: string;
|
|
99
|
+
notNull: true;
|
|
100
|
+
hasDefault: false;
|
|
101
|
+
isPrimaryKey: false;
|
|
102
|
+
isAutoincrement: false;
|
|
103
|
+
hasRuntimeDefault: false;
|
|
104
|
+
enumValues: [string, ...string[]];
|
|
105
|
+
baseColumn: never;
|
|
106
|
+
identity: undefined;
|
|
107
|
+
generated: undefined;
|
|
108
|
+
}, {}, {}>;
|
|
109
|
+
version: import("drizzle-orm/pg-core").PgColumn<{
|
|
110
|
+
name: "version";
|
|
111
|
+
tableName: "hf_run";
|
|
112
|
+
dataType: "string";
|
|
113
|
+
columnType: "PgText";
|
|
114
|
+
data: string;
|
|
115
|
+
driverParam: string;
|
|
116
|
+
notNull: false;
|
|
117
|
+
hasDefault: false;
|
|
118
|
+
isPrimaryKey: false;
|
|
119
|
+
isAutoincrement: false;
|
|
120
|
+
hasRuntimeDefault: false;
|
|
121
|
+
enumValues: [string, ...string[]];
|
|
122
|
+
baseColumn: never;
|
|
123
|
+
identity: undefined;
|
|
124
|
+
generated: undefined;
|
|
125
|
+
}, {}, {}>;
|
|
126
|
+
recordType: import("drizzle-orm/pg-core").PgColumn<{
|
|
127
|
+
name: "record_type";
|
|
128
|
+
tableName: "hf_run";
|
|
129
|
+
dataType: "string";
|
|
130
|
+
columnType: "PgText";
|
|
131
|
+
data: string;
|
|
132
|
+
driverParam: string;
|
|
133
|
+
notNull: false;
|
|
134
|
+
hasDefault: false;
|
|
135
|
+
isPrimaryKey: false;
|
|
136
|
+
isAutoincrement: false;
|
|
137
|
+
hasRuntimeDefault: false;
|
|
138
|
+
enumValues: [string, ...string[]];
|
|
139
|
+
baseColumn: never;
|
|
140
|
+
identity: undefined;
|
|
141
|
+
generated: undefined;
|
|
142
|
+
}, {}, {}>;
|
|
143
|
+
recordId: import("drizzle-orm/pg-core").PgColumn<{
|
|
144
|
+
name: "record_id";
|
|
145
|
+
tableName: "hf_run";
|
|
146
|
+
dataType: "string";
|
|
147
|
+
columnType: "PgText";
|
|
148
|
+
data: string;
|
|
149
|
+
driverParam: string;
|
|
150
|
+
notNull: false;
|
|
151
|
+
hasDefault: false;
|
|
152
|
+
isPrimaryKey: false;
|
|
153
|
+
isAutoincrement: false;
|
|
154
|
+
hasRuntimeDefault: false;
|
|
155
|
+
enumValues: [string, ...string[]];
|
|
156
|
+
baseColumn: never;
|
|
157
|
+
identity: undefined;
|
|
158
|
+
generated: undefined;
|
|
159
|
+
}, {}, {}>;
|
|
160
|
+
error: import("drizzle-orm/pg-core").PgColumn<{
|
|
161
|
+
name: "error";
|
|
162
|
+
tableName: "hf_run";
|
|
163
|
+
dataType: "string";
|
|
164
|
+
columnType: "PgText";
|
|
165
|
+
data: string;
|
|
166
|
+
driverParam: string;
|
|
167
|
+
notNull: false;
|
|
168
|
+
hasDefault: false;
|
|
169
|
+
isPrimaryKey: false;
|
|
170
|
+
isAutoincrement: false;
|
|
171
|
+
hasRuntimeDefault: false;
|
|
172
|
+
enumValues: [string, ...string[]];
|
|
173
|
+
baseColumn: never;
|
|
174
|
+
identity: undefined;
|
|
175
|
+
generated: undefined;
|
|
176
|
+
}, {}, {}>;
|
|
177
|
+
startedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
178
|
+
name: "started_at";
|
|
179
|
+
tableName: "hf_run";
|
|
180
|
+
dataType: "date";
|
|
181
|
+
columnType: "PgTimestamp";
|
|
182
|
+
data: Date;
|
|
183
|
+
driverParam: string;
|
|
184
|
+
notNull: true;
|
|
185
|
+
hasDefault: true;
|
|
186
|
+
isPrimaryKey: false;
|
|
187
|
+
isAutoincrement: false;
|
|
188
|
+
hasRuntimeDefault: false;
|
|
189
|
+
enumValues: undefined;
|
|
190
|
+
baseColumn: never;
|
|
191
|
+
identity: undefined;
|
|
192
|
+
generated: undefined;
|
|
193
|
+
}, {}, {}>;
|
|
194
|
+
finishedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
195
|
+
name: "finished_at";
|
|
196
|
+
tableName: "hf_run";
|
|
197
|
+
dataType: "date";
|
|
198
|
+
columnType: "PgTimestamp";
|
|
199
|
+
data: Date;
|
|
200
|
+
driverParam: string;
|
|
201
|
+
notNull: false;
|
|
202
|
+
hasDefault: false;
|
|
203
|
+
isPrimaryKey: false;
|
|
204
|
+
isAutoincrement: false;
|
|
205
|
+
hasRuntimeDefault: false;
|
|
206
|
+
enumValues: undefined;
|
|
207
|
+
baseColumn: never;
|
|
208
|
+
identity: undefined;
|
|
209
|
+
generated: undefined;
|
|
210
|
+
}, {}, {}>;
|
|
211
|
+
};
|
|
212
|
+
dialect: "pg";
|
|
213
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { check, index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
3
|
+
export const runStatuses = ["running", "waiting", "paused", "done", "failed"];
|
|
4
|
+
export const hfRun = pgTable("hf_run", {
|
|
5
|
+
runId: text("run_id").primaryKey(),
|
|
6
|
+
flow: text("flow").notNull(),
|
|
7
|
+
input: jsonb("input"),
|
|
8
|
+
status: text("status", { enum: runStatuses }).notNull(),
|
|
9
|
+
attempt: integer("attempt").notNull().default(1),
|
|
10
|
+
// The fencing token: attempt 1 is `run_id`, attempt N is `run_id:N`. Written
|
|
11
|
+
// only by the one bump path under FOR UPDATE; every step write takes FOR SHARE
|
|
12
|
+
// on this row and matches it.
|
|
13
|
+
currentWorkflowId: text("current_workflow_id").notNull().unique(),
|
|
14
|
+
version: text("version"),
|
|
15
|
+
recordType: text("record_type"),
|
|
16
|
+
recordId: text("record_id"),
|
|
17
|
+
error: text("error"),
|
|
18
|
+
startedAt: timestamp("started_at", { withTimezone: true, mode: "date" })
|
|
19
|
+
.notNull()
|
|
20
|
+
.defaultNow(),
|
|
21
|
+
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "date" }),
|
|
22
|
+
}, (t) => [
|
|
23
|
+
index("hf_run_status_idx").on(t.status),
|
|
24
|
+
index("hf_run_record_idx").on(t.recordType, t.recordId),
|
|
25
|
+
check("hf_run_attempt_positive", sql `${t.attempt} >= 1`),
|
|
26
|
+
]);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Pool, type PoolConfig } from "pg";
|
|
3
|
+
import * as schema from "./schema/index.js";
|
|
4
|
+
export declare const STEP_POOL_SIZE = 8;
|
|
5
|
+
/** `ctx.tx`'s first statement, and the only place the fencing token is read. */
|
|
6
|
+
export declare const FENCE_STATEMENT = "SELECT 1 FROM hf_run WHERE run_id = $1 AND current_workflow_id = $2 FOR SHARE";
|
|
7
|
+
export declare class StaleAttempt extends Error {
|
|
8
|
+
readonly runId: string;
|
|
9
|
+
readonly workflowId: string;
|
|
10
|
+
constructor(runId: string, workflowId: string);
|
|
11
|
+
}
|
|
12
|
+
export type StepDatabase = NodePgDatabase<typeof schema>;
|
|
13
|
+
export type StepPoolOptions = PoolConfig;
|
|
14
|
+
export interface StepPool {
|
|
15
|
+
/** The fenced `pg.Pool`: reads pass, writes are refused outside `ctx.tx`. */
|
|
16
|
+
readonly pool: Pool;
|
|
17
|
+
/** What an app's `@/db` resolves to in the worker. */
|
|
18
|
+
readonly db: StepDatabase;
|
|
19
|
+
tx<T>(runId: string, workflowId: string, work: (tx: StepDatabase) => Promise<T>): Promise<T>;
|
|
20
|
+
end(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The only database handle step and app code are meant to reach. `runId`/`workflowId` are
|
|
24
|
+
* explicit here; chunk 9 supplies them from the DBOS context.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createStepPool(options: StepPoolOptions): StepPool;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import { fencePool, tagForTransaction } from "./fenced-client.js";
|
|
4
|
+
import * as schema from "./schema/index.js";
|
|
5
|
+
export const STEP_POOL_SIZE = 8;
|
|
6
|
+
/** `ctx.tx`'s first statement, and the only place the fencing token is read. */
|
|
7
|
+
export const FENCE_STATEMENT = "SELECT 1 FROM hf_run WHERE run_id = $1 AND current_workflow_id = $2 FOR SHARE";
|
|
8
|
+
export class StaleAttempt extends Error {
|
|
9
|
+
runId;
|
|
10
|
+
workflowId;
|
|
11
|
+
constructor(runId, workflowId) {
|
|
12
|
+
super(`StaleAttempt: hf_run ${runId} is no longer on attempt ${workflowId}`);
|
|
13
|
+
this.name = "StaleAttempt";
|
|
14
|
+
this.runId = runId;
|
|
15
|
+
this.workflowId = workflowId;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The only database handle step and app code are meant to reach. `runId`/`workflowId` are
|
|
20
|
+
* explicit here; chunk 9 supplies them from the DBOS context.
|
|
21
|
+
*/
|
|
22
|
+
export function createStepPool(options) {
|
|
23
|
+
const pool = fencePool(new Pool({ max: STEP_POOL_SIZE, ...options }));
|
|
24
|
+
return {
|
|
25
|
+
pool,
|
|
26
|
+
db: drizzle(pool, { schema }),
|
|
27
|
+
async tx(runId, workflowId, work) {
|
|
28
|
+
const client = await pool.connect();
|
|
29
|
+
tagForTransaction(client);
|
|
30
|
+
let broken;
|
|
31
|
+
try {
|
|
32
|
+
await client.query("BEGIN");
|
|
33
|
+
const fence = await client.query(FENCE_STATEMENT, [runId, workflowId]);
|
|
34
|
+
if (fence.rowCount === 0)
|
|
35
|
+
throw new StaleAttempt(runId, workflowId);
|
|
36
|
+
const result = await work(drizzle(client, { schema }));
|
|
37
|
+
await client.query("COMMIT");
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
try {
|
|
42
|
+
await client.query("ROLLBACK");
|
|
43
|
+
}
|
|
44
|
+
catch (rollbackError) {
|
|
45
|
+
// The connection is in an unknown transaction state; hand it back with the error
|
|
46
|
+
// so the pool destroys it instead of leasing it to the next transaction.
|
|
47
|
+
broken = rollbackError;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
client.release(broken);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
end: () => pool.end(),
|
|
56
|
+
};
|
|
57
|
+
}
|