@smithers-orchestrator/db 0.16.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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +43 -0
  3. package/src/JsonBounds.ts +6 -0
  4. package/src/SchemaRegistryEntry.ts +6 -0
  5. package/src/SqlMessageStorage.js +818 -0
  6. package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
  7. package/src/SqliteWriteRetryOptions.ts +7 -0
  8. package/src/adapter/AlertRow.ts +29 -0
  9. package/src/adapter/AlertSeverity.ts +2 -0
  10. package/src/adapter/AlertStatus.ts +2 -0
  11. package/src/adapter/ApprovalRow.ts +13 -0
  12. package/src/adapter/AttemptRow.ts +17 -0
  13. package/src/adapter/CacheRow.ts +12 -0
  14. package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
  15. package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
  16. package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
  17. package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
  18. package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
  19. package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
  20. package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
  21. package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
  22. package/src/adapter/EventHistoryQuery.ts +7 -0
  23. package/src/adapter/HumanRequestRow.ts +19 -0
  24. package/src/adapter/NodeDiffCacheRow.ts +9 -0
  25. package/src/adapter/NodeRow.ts +10 -0
  26. package/src/adapter/PendingHumanRequestRow.ts +7 -0
  27. package/src/adapter/RunAncestryRow.ts +5 -0
  28. package/src/adapter/RunRow.ts +21 -0
  29. package/src/adapter/SignalQuery.ts +6 -0
  30. package/src/adapter/SignalRow.ts +9 -0
  31. package/src/adapter/SmithersDb.js +2236 -0
  32. package/src/adapter/StaleRunRecord.ts +7 -0
  33. package/src/adapter/index.js +27 -0
  34. package/src/adapter.js +2359 -0
  35. package/src/assertJsonPayloadWithinBounds.js +94 -0
  36. package/src/assertMaxBytes.js +23 -0
  37. package/src/assertMaxJsonDepth.js +40 -0
  38. package/src/assertMaxStringLength.js +16 -0
  39. package/src/assertOptionalArrayMaxLength.js +16 -0
  40. package/src/assertOptionalStringMaxLength.js +11 -0
  41. package/src/assertPositiveFiniteInteger.js +14 -0
  42. package/src/assertPositiveFiniteNumber.js +12 -0
  43. package/src/buildHumanRequestId.js +9 -0
  44. package/src/cache/nodeDiffCache.js +124 -0
  45. package/src/ensure.js +18 -0
  46. package/src/ensureSqlMessageStorage.js +11 -0
  47. package/src/ensureSqlMessageStorageEffect.js +12 -0
  48. package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
  49. package/src/frame-codec/FrameDelta.ts +6 -0
  50. package/src/frame-codec/FrameDeltaOp.ts +20 -0
  51. package/src/frame-codec/FrameEncoding.ts +1 -0
  52. package/src/frame-codec/JsonPath.ts +3 -0
  53. package/src/frame-codec/JsonPathSegment.ts +1 -0
  54. package/src/frame-codec/applyFrameDelta.js +143 -0
  55. package/src/frame-codec/applyFrameDeltaJson.js +10 -0
  56. package/src/frame-codec/encodeFrameDelta.js +247 -0
  57. package/src/frame-codec/index.js +15 -0
  58. package/src/frame-codec/normalizeFrameEncoding.js +13 -0
  59. package/src/frame-codec/parseFrameDelta.js +27 -0
  60. package/src/frame-codec/serializeFrameDelta.js +9 -0
  61. package/src/frame-codec.js +409 -0
  62. package/src/getSqlMessageStorage.js +11 -0
  63. package/src/index.d.ts +5203 -0
  64. package/src/index.js +20 -0
  65. package/src/input-bounds.js +12 -0
  66. package/src/input.js +17 -0
  67. package/src/internal-schema/index.js +19 -0
  68. package/src/internal-schema/smithersAlerts.js +27 -0
  69. package/src/internal-schema/smithersApprovals.js +18 -0
  70. package/src/internal-schema/smithersAttempts.js +20 -0
  71. package/src/internal-schema/smithersCache.js +13 -0
  72. package/src/internal-schema/smithersCron.js +11 -0
  73. package/src/internal-schema/smithersEvents.js +10 -0
  74. package/src/internal-schema/smithersFrames.js +14 -0
  75. package/src/internal-schema/smithersHumanRequests.js +17 -0
  76. package/src/internal-schema/smithersNodeDiffs.js +12 -0
  77. package/src/internal-schema/smithersNodes.js +13 -0
  78. package/src/internal-schema/smithersRalph.js +10 -0
  79. package/src/internal-schema/smithersRuns.js +22 -0
  80. package/src/internal-schema/smithersSandboxes.js +16 -0
  81. package/src/internal-schema/smithersSignals.js +12 -0
  82. package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
  83. package/src/internal-schema/smithersToolCalls.js +19 -0
  84. package/src/internal-schema/smithersVectors.js +12 -0
  85. package/src/internal-schema.js +245 -0
  86. package/src/isRetryableSqliteWriteError.js +53 -0
  87. package/src/loadInputEffect.js +28 -0
  88. package/src/loadOutputsEffect.js +87 -0
  89. package/src/output/OutputKey.ts +1 -0
  90. package/src/output/buildKeyWhere.js +17 -0
  91. package/src/output/buildOutputRow.js +34 -0
  92. package/src/output/describeSchemaShape.js +70 -0
  93. package/src/output/getAgentOutputSchema.js +13 -0
  94. package/src/output/getKeyColumns.js +19 -0
  95. package/src/output/index.js +14 -0
  96. package/src/output/selectOutputRowEffect.js +30 -0
  97. package/src/output/stripAutoColumns.js +10 -0
  98. package/src/output/upsertOutputRowEffect.js +38 -0
  99. package/src/output/validateExistingOutput.js +17 -0
  100. package/src/output/validateOutput.js +17 -0
  101. package/src/output-schema-descriptor.js +163 -0
  102. package/src/output.js +240 -0
  103. package/src/react-output.js +10 -0
  104. package/src/runState/ComputeRunStateOptions.ts +4 -0
  105. package/src/runState/DeriveRunStateInput.ts +10 -0
  106. package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
  107. package/src/runState/ReasonBlocked.ts +10 -0
  108. package/src/runState/ReasonUnhealthy.ts +6 -0
  109. package/src/runState/RunState.ts +12 -0
  110. package/src/runState/RunStateView.ts +11 -0
  111. package/src/runState/computeRunState.js +22 -0
  112. package/src/runState/computeRunStateFromRow.js +102 -0
  113. package/src/runState/deriveRunState.js +109 -0
  114. package/src/runState/parseEventMeta.js +18 -0
  115. package/src/runState/parseTimerMeta.js +16 -0
  116. package/src/runState-types.ts +23 -0
  117. package/src/runState.js +7 -0
  118. package/src/schema-signature.js +22 -0
  119. package/src/snapshot.js +125 -0
  120. package/src/sql-message-storage.js +839 -0
  121. package/src/storage/InMemoryStorage.js +484 -0
  122. package/src/storage/StorageService.js +7 -0
  123. package/src/storage/StorageServiceShape.ts +122 -0
  124. package/src/storage/StorageServiceTypes.ts +150 -0
  125. package/src/unwrapZodType.js +17 -0
  126. package/src/utils/camelToSnake.js +6 -0
  127. package/src/withSqliteWriteRetryEffect.js +110 -0
  128. package/src/write-retry.js +49 -0
  129. package/src/zodToCreateTableSQL.js +41 -0
  130. package/src/zodToTable.js +60 -0
@@ -0,0 +1,150 @@
1
+ export type JsonRecord = Record<string, unknown>;
2
+ export type OutputKey = Record<string, string | number | boolean | null>;
3
+
4
+ export type Run = {
5
+ readonly runId: string;
6
+ readonly parentRunId?: string | null;
7
+ readonly workflowName?: string | null;
8
+ readonly workflowPath?: string | null;
9
+ readonly workflowHash?: string | null;
10
+ readonly status: string;
11
+ readonly createdAtMs?: number;
12
+ readonly startedAtMs?: number | null;
13
+ readonly finishedAtMs?: number | null;
14
+ readonly heartbeatAtMs?: number | null;
15
+ readonly runtimeOwnerId?: string | null;
16
+ readonly cancelRequestedAtMs?: number | null;
17
+ readonly hijackRequestedAtMs?: number | null;
18
+ readonly hijackTarget?: string | null;
19
+ readonly vcsType?: string | null;
20
+ readonly vcsRoot?: string | null;
21
+ readonly vcsRevision?: string | null;
22
+ readonly errorJson?: string | null;
23
+ readonly configJson?: string | null;
24
+ };
25
+
26
+ export type RunPatch = Partial<Omit<Run, "runId">>;
27
+
28
+ export type RunAncestryRow = {
29
+ readonly runId: string;
30
+ readonly parentRunId: string | null;
31
+ readonly depth: number;
32
+ };
33
+
34
+ export type Attempt = {
35
+ readonly runId: string;
36
+ readonly nodeId: string;
37
+ readonly iteration: number;
38
+ readonly attempt: number;
39
+ readonly state: string;
40
+ readonly startedAtMs: number;
41
+ readonly finishedAtMs?: number | null;
42
+ readonly heartbeatAtMs?: number | null;
43
+ readonly heartbeatDataJson?: string | null;
44
+ readonly errorJson?: string | null;
45
+ readonly jjPointer?: string | null;
46
+ readonly responseText?: string | null;
47
+ readonly jjCwd?: string | null;
48
+ readonly cached?: boolean;
49
+ readonly metaJson?: string | null;
50
+ };
51
+
52
+ export type AttemptPatch = Partial<
53
+ Omit<Attempt, "runId" | "nodeId" | "iteration" | "attempt">
54
+ >;
55
+
56
+ export type FrameRow = {
57
+ readonly runId: string;
58
+ readonly frameNo: number;
59
+ readonly xmlJson?: string | null;
60
+ readonly graphJson?: string | null;
61
+ readonly xmlHash?: string | null;
62
+ readonly xmlEncoding?: string | null;
63
+ readonly createdAtMs?: number;
64
+ readonly [key: string]: unknown;
65
+ };
66
+
67
+ export type SignalInsertRow = {
68
+ readonly runId: string;
69
+ readonly signalName: string;
70
+ readonly correlationId: string | null;
71
+ readonly payloadJson: string;
72
+ readonly receivedAtMs: number;
73
+ readonly receivedBy?: string | null;
74
+ };
75
+
76
+ export type EventRow = {
77
+ readonly runId: string;
78
+ readonly seq: number;
79
+ readonly timestampMs: number;
80
+ readonly type: string;
81
+ readonly payloadJson: string;
82
+ };
83
+
84
+ export type EventInsertRow = Omit<EventRow, "seq">;
85
+
86
+ export type RalphRow = {
87
+ readonly runId: string;
88
+ readonly ralphId: string;
89
+ readonly iteration: number;
90
+ readonly done?: boolean;
91
+ readonly stateJson?: string | null;
92
+ readonly updatedAtMs?: number;
93
+ };
94
+
95
+ export type SandboxRow = {
96
+ readonly runId: string;
97
+ readonly sandboxId: string;
98
+ readonly [key: string]: unknown;
99
+ };
100
+
101
+ export type ToolCallRow = {
102
+ readonly runId: string;
103
+ readonly nodeId: string;
104
+ readonly iteration: number;
105
+ readonly [key: string]: unknown;
106
+ };
107
+
108
+ export type CronRow = {
109
+ readonly cronId: string;
110
+ readonly pattern?: string;
111
+ readonly workflowPath?: string;
112
+ readonly enabled?: boolean;
113
+ readonly lastRunAtMs?: number | null;
114
+ readonly nextRunAtMs?: number | null;
115
+ readonly errorJson?: string | null;
116
+ readonly [key: string]: unknown;
117
+ };
118
+
119
+ export type ScorerResultRow = {
120
+ readonly runId: string;
121
+ readonly nodeId?: string;
122
+ readonly scorerId?: string;
123
+ readonly scoredAtMs?: number;
124
+ readonly [key: string]: unknown;
125
+ };
126
+
127
+ export type ClaimRunForResumeParams = {
128
+ readonly runId: string;
129
+ readonly expectedStatus?: string;
130
+ readonly expectedRuntimeOwnerId: string | null;
131
+ readonly expectedHeartbeatAtMs: number | null;
132
+ readonly staleBeforeMs: number;
133
+ readonly claimOwnerId: string;
134
+ readonly claimHeartbeatAtMs: number;
135
+ readonly requireStale?: boolean;
136
+ };
137
+
138
+ export type ReleaseRunResumeClaimParams = {
139
+ readonly runId: string;
140
+ readonly claimOwnerId: string;
141
+ readonly restoreRuntimeOwnerId: string | null;
142
+ readonly restoreHeartbeatAtMs: number | null;
143
+ };
144
+
145
+ export type UpdateClaimedRunParams = {
146
+ readonly runId: string;
147
+ readonly expectedRuntimeOwnerId: string;
148
+ readonly expectedHeartbeatAtMs: number | null;
149
+ readonly patch: RunPatch;
150
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Unwraps Zod wrapper types (nullable, optional, default) to get the base type.
3
+ */
4
+ export function unwrapZodType(t) {
5
+ if (!t)
6
+ return t;
7
+ if (t._zod?.def) {
8
+ const typeName = t._zod.def.type;
9
+ if (typeName === "nullable" ||
10
+ typeName === "optional" ||
11
+ typeName === "default") {
12
+ const inner = t._zod.def.innerType;
13
+ return inner ? unwrapZodType(inner) : t;
14
+ }
15
+ }
16
+ return t;
17
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Converts a camelCase string to snake_case.
3
+ */
4
+ export function camelToSnake(str) {
5
+ return str.replace(/([A-Z])/g, "_$1").toLowerCase();
6
+ }
@@ -0,0 +1,110 @@
1
+ import { Duration, Effect, Metric, Schedule, ScheduleDecision, ScheduleIntervals, } from "effect";
2
+ import { dbRetries } from "@smithers-orchestrator/observability/metrics";
3
+ import { retryPolicyToSchedule } from "@smithers-orchestrator/scheduler/retryPolicyToSchedule";
4
+ import { isRetryableSqliteWriteError } from "./isRetryableSqliteWriteError.js";
5
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
6
+ /** @typedef {import("./SqliteWriteRetryOptions.ts").SqliteWriteRetryOptions} SqliteWriteRetryOptions */
7
+
8
+ // Raised from 6→10 and 2000→10000: concurrent Worktree tasks can produce
9
+ // short bursts of SQLITE_IOERR_VNODE on macOS; more retries with a wider
10
+ // window gives busy_timeout time to clear the VFS lock.
11
+ const DEFAULT_MAX_ATTEMPTS = 10;
12
+ const DEFAULT_BASE_DELAY_MS = 50;
13
+ const DEFAULT_MAX_DELAY_MS = 10_000;
14
+ /**
15
+ * @param {unknown} error
16
+ * @returns {string}
17
+ */
18
+ function describeSqliteWriteError(error) {
19
+ const metadata = findSqliteErrorMetadata(error);
20
+ const code = metadata?.code ?? "";
21
+ const message = metadata?.message || String(error?.message ?? error ?? "unknown error");
22
+ return code ? `${code}: ${message}` : message;
23
+ }
24
+ /**
25
+ * @param {unknown} error
26
+ * @returns {SqliteErrorMetadata | null}
27
+ */
28
+ function readSqliteErrorMetadata(error) {
29
+ if (!error || (typeof error !== "object" && !(error instanceof Error))) {
30
+ return null;
31
+ }
32
+ const code = typeof error?.code === "string" ? error.code : "";
33
+ const message = String(error?.message ?? "");
34
+ return { code, message };
35
+ }
36
+ /**
37
+ * @param {unknown} error
38
+ * @returns {SqliteErrorMetadata | null}
39
+ */
40
+ function findSqliteErrorMetadata(error) {
41
+ const seen = new Set();
42
+ let current = error;
43
+ while (current && !seen.has(current)) {
44
+ seen.add(current);
45
+ const metadata = readSqliteErrorMetadata(current);
46
+ if (metadata) {
47
+ const message = metadata.message.toLowerCase();
48
+ if (metadata.code.startsWith("SQLITE_BUSY") ||
49
+ metadata.code.startsWith("SQLITE_IOERR") ||
50
+ message.includes("database is locked") ||
51
+ message.includes("database is busy") ||
52
+ message.includes("disk i/o error")) {
53
+ return metadata;
54
+ }
55
+ }
56
+ current = current?.cause;
57
+ }
58
+ return readSqliteErrorMetadata(error);
59
+ }
60
+ /**
61
+ * @param {number} maxAttempts
62
+ * @param {number} baseDelayMs
63
+ * @param {number} maxDelayMs
64
+ */
65
+ function makeSqliteRetrySchedule(maxAttempts, baseDelayMs, maxDelayMs) {
66
+ const boundedBaseDelayMs = Math.max(1, Math.floor(baseDelayMs));
67
+ const boundedMaxDelayMs = Math.max(1, Math.floor(maxDelayMs));
68
+ return retryPolicyToSchedule({
69
+ backoff: "exponential",
70
+ initialDelayMs: boundedBaseDelayMs,
71
+ }).pipe(Schedule.modifyDelay((_, delay) => Duration.millis(Math.min(boundedMaxDelayMs, Duration.toMillis(delay)))), Schedule.jitteredWith({ min: 0.75, max: 1.25 }), Schedule.whileInput(isRetryableSqliteWriteError), Schedule.intersect(Schedule.recurs(Math.max(0, maxAttempts - 1))));
72
+ }
73
+ /**
74
+ * @template A
75
+ * @param {() => Effect.Effect<A, SmithersError>} operation
76
+ * @param {SqliteWriteRetryOptions} [opts]
77
+ * @returns {Effect.Effect<A, SmithersError>}
78
+ */
79
+ export function withSqliteWriteRetryEffect(operation, opts = {}) {
80
+ const { label = "sqlite write", maxAttempts = DEFAULT_MAX_ATTEMPTS, baseDelayMs = DEFAULT_BASE_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, sleep, } = opts;
81
+ const boundedMaxAttempts = Math.max(1, Math.floor(maxAttempts));
82
+ let retryAttempt = 0;
83
+ let lastRetryError;
84
+ const retrySchedule = Schedule.mapInput(makeSqliteRetrySchedule(boundedMaxAttempts, baseDelayMs, maxDelayMs), (error) => {
85
+ lastRetryError = error;
86
+ return error;
87
+ }).pipe(Schedule.onDecision((_, decision) => {
88
+ if (ScheduleDecision.isDone(decision) || !lastRetryError) {
89
+ return Effect.void;
90
+ }
91
+ retryAttempt += 1;
92
+ const delayMs = Math.max(1, Math.round(ScheduleIntervals.start(decision.intervals) - Date.now()));
93
+ return Effect.gen(function* () {
94
+ yield* Metric.increment(dbRetries);
95
+ yield* Effect.logWarning(`${label} failed with ${describeSqliteWriteError(lastRetryError)}; retrying in ${delayMs}ms (${retryAttempt}/${boundedMaxAttempts})`);
96
+ if (sleep) {
97
+ yield* Effect.promise(() => sleep(delayMs));
98
+ }
99
+ }).pipe(Effect.annotateLogs({
100
+ retryable: true,
101
+ retryAttempt,
102
+ retryMaxAttempts: boundedMaxAttempts,
103
+ retryDelayMs: delayMs,
104
+ retryLabel: label,
105
+ }));
106
+ }));
107
+ return Effect.suspend(operation).pipe(Effect.retry(sleep
108
+ ? Schedule.modifyDelay(retrySchedule, () => Duration.zero)
109
+ : retrySchedule), Effect.withLogSpan("sqlite-write-retry"));
110
+ }
@@ -0,0 +1,49 @@
1
+ import { isRetryableSqliteWriteError } from "./isRetryableSqliteWriteError.js";
2
+ /** @typedef {import("./SqliteWriteRetryOptions.ts").SqliteWriteRetryOptions} SqliteWriteRetryOptions */
3
+
4
+ const DEFAULT_MAX_ATTEMPTS = 6;
5
+ const DEFAULT_BASE_DELAY_MS = 50;
6
+ const DEFAULT_MAX_DELAY_MS = 2_000;
7
+ export { isRetryableSqliteWriteError } from "./isRetryableSqliteWriteError.js";
8
+ export { withSqliteWriteRetryEffect } from "./withSqliteWriteRetryEffect.js";
9
+ /**
10
+ * @param {number} ms
11
+ * @returns {Promise<void>}
12
+ */
13
+ function delay(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ /**
17
+ * @param {number} retryAttempt
18
+ * @param {number} baseDelayMs
19
+ * @param {number} maxDelayMs
20
+ * @returns {number}
21
+ */
22
+ function computeDelayMs(retryAttempt, baseDelayMs, maxDelayMs) {
23
+ const boundedBaseDelayMs = Math.max(1, Math.floor(baseDelayMs));
24
+ const boundedMaxDelayMs = Math.max(1, Math.floor(maxDelayMs));
25
+ const exponentialDelayMs = boundedBaseDelayMs * 2 ** Math.max(0, retryAttempt - 1);
26
+ return Math.min(boundedMaxDelayMs, exponentialDelayMs);
27
+ }
28
+ /**
29
+ * @template A
30
+ * @param {() => A | PromiseLike<A>} operation
31
+ * @param {SqliteWriteRetryOptions} [opts]
32
+ * @returns {Promise<A>}
33
+ */
34
+ export async function withSqliteWriteRetry(operation, opts = {}) {
35
+ const { maxAttempts = DEFAULT_MAX_ATTEMPTS, baseDelayMs = DEFAULT_BASE_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, sleep = delay, } = opts;
36
+ const boundedMaxAttempts = Math.max(1, Math.floor(maxAttempts));
37
+ for (let attempt = 1;; attempt += 1) {
38
+ try {
39
+ return await operation();
40
+ }
41
+ catch (error) {
42
+ if (attempt >= boundedMaxAttempts ||
43
+ !isRetryableSqliteWriteError(error)) {
44
+ throw error;
45
+ }
46
+ await sleep(computeDelayMs(attempt, baseDelayMs, maxDelayMs));
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { unwrapZodType } from "./unwrapZodType.js";
3
+ import { camelToSnake } from "./utils/camelToSnake.js";
4
+ /**
5
+ * Determines the Zod base type name from a (possibly unwrapped) Zod type.
6
+ */
7
+ function getZodBaseTypeName(zodType) {
8
+ return zodType._zod?.def?.type ?? "unknown";
9
+ }
10
+ /**
11
+ * Generates a CREATE TABLE IF NOT EXISTS SQL statement from a Zod schema.
12
+ * Used for runtime table creation without Drizzle migrations.
13
+ */
14
+ export function zodToCreateTableSQL(tableName, schema, opts) {
15
+ const colDefs = opts?.isInput
16
+ ? [`run_id TEXT NOT NULL PRIMARY KEY`]
17
+ : [
18
+ `run_id TEXT NOT NULL`,
19
+ `node_id TEXT NOT NULL`,
20
+ `iteration INTEGER NOT NULL DEFAULT 0`,
21
+ ];
22
+ const shape = schema.shape;
23
+ for (const [key] of Object.entries(shape)) {
24
+ const colName = camelToSnake(key);
25
+ const baseType = unwrapZodType(shape[key]);
26
+ const baseTypeName = getZodBaseTypeName(baseType);
27
+ if (baseTypeName === "number" ||
28
+ baseTypeName === "int" ||
29
+ baseTypeName === "float" ||
30
+ baseTypeName === "boolean") {
31
+ colDefs.push(`"${colName}" INTEGER`);
32
+ }
33
+ else {
34
+ colDefs.push(`"${colName}" TEXT`);
35
+ }
36
+ }
37
+ if (!opts?.isInput) {
38
+ colDefs.push(`PRIMARY KEY (run_id, node_id, iteration)`);
39
+ }
40
+ return `CREATE TABLE IF NOT EXISTS "${tableName}" (${colDefs.join(", ")})`;
41
+ }
@@ -0,0 +1,60 @@
1
+ import { sqliteTable, text, integer, primaryKey, } from "drizzle-orm/sqlite-core";
2
+ import { z } from "zod";
3
+ import { unwrapZodType } from "./unwrapZodType.js";
4
+ import { camelToSnake } from "./utils/camelToSnake.js";
5
+ /**
6
+ * Determines the Zod base type name from a (possibly unwrapped) Zod type.
7
+ */
8
+ function getZodBaseTypeName(zodType) {
9
+ return zodType._zod?.def?.type ?? "unknown";
10
+ }
11
+ /**
12
+ * Generates a Drizzle sqliteTable from a Zod object schema.
13
+ *
14
+ * Each Zod field is mapped to a SQLite column:
15
+ * - z.string() / z.enum() -> text column
16
+ * - z.number() -> integer column
17
+ * - z.boolean() -> integer column with boolean mode
18
+ * - z.array() / z.object() / complex -> text column with json mode
19
+ *
20
+ * All tables include standard smithers key columns:
21
+ * runId, nodeId, iteration with a composite primary key.
22
+ */
23
+ export function zodToTable(tableName, schema, opts) {
24
+ const columns = opts?.isInput
25
+ ? { runId: text("run_id").primaryKey() }
26
+ : {
27
+ runId: text("run_id").notNull(),
28
+ nodeId: text("node_id").notNull(),
29
+ iteration: integer("iteration").notNull().default(0),
30
+ };
31
+ const shape = schema.shape;
32
+ for (const [key, zodType] of Object.entries(shape)) {
33
+ const colName = camelToSnake(key);
34
+ const baseType = unwrapZodType(zodType);
35
+ const baseTypeName = getZodBaseTypeName(baseType);
36
+ if (baseTypeName === "number" ||
37
+ baseTypeName === "int" ||
38
+ baseTypeName === "float") {
39
+ columns[key] = integer(colName);
40
+ }
41
+ else if (baseTypeName === "boolean") {
42
+ columns[key] = integer(colName, { mode: "boolean" });
43
+ }
44
+ else if (baseTypeName === "string" ||
45
+ baseTypeName === "enum" ||
46
+ baseTypeName === "literal") {
47
+ columns[key] = text(colName);
48
+ }
49
+ else {
50
+ // arrays, objects, unions, and anything complex -> JSON text
51
+ columns[key] = text(colName, { mode: "json" });
52
+ }
53
+ }
54
+ if (opts?.isInput) {
55
+ return sqliteTable(tableName, columns);
56
+ }
57
+ return sqliteTable(tableName, columns, (t) => [
58
+ primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }),
59
+ ]);
60
+ }