@sixb/core 0.1.0 → 0.1.1

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 (214) hide show
  1. package/README.md +24 -4
  2. package/dist/actions/index.js +17 -17
  3. package/dist/actions/worker.js +7 -7
  4. package/dist/actions/worker.js.map +1 -1
  5. package/dist/agents/adapters.d.ts.map +1 -1
  6. package/dist/agents/builders.d.ts +8 -1
  7. package/dist/agents/builders.d.ts.map +1 -1
  8. package/dist/agents/context.js +2 -2
  9. package/dist/agents/errors.d.ts +15 -0
  10. package/dist/agents/errors.d.ts.map +1 -1
  11. package/dist/agents/index.d.ts +4 -4
  12. package/dist/agents/index.d.ts.map +1 -1
  13. package/dist/agents/index.js +30 -11
  14. package/dist/agents/index.js.map +1 -1
  15. package/dist/agents/tool-definition.d.ts +9 -0
  16. package/dist/agents/tool-definition.d.ts.map +1 -0
  17. package/dist/agents/types.d.ts +64 -0
  18. package/dist/agents/types.d.ts.map +1 -1
  19. package/dist/agents/validation.d.ts +14 -1
  20. package/dist/agents/validation.d.ts.map +1 -1
  21. package/dist/auth/index.js +4 -4
  22. package/dist/authorization/index.js +3 -3
  23. package/dist/blob-storage/browser.js +3 -2
  24. package/dist/blob-storage/browser.js.map +1 -1
  25. package/dist/blob-storage/index.js +4 -3
  26. package/dist/blob-storage/index.js.map +1 -1
  27. package/dist/blob-storage/validation.d.ts.map +1 -1
  28. package/dist/bootstrap/index.js +25 -25
  29. package/dist/broker/index.js +2 -2
  30. package/dist/{chunk-590dejbp.js → chunk-4hwxq5e6.js} +1 -1
  31. package/dist/{chunk-wj01s5hr.js → chunk-4tvj3ate.js} +1 -1
  32. package/dist/{chunk-tsk2bxs3.js → chunk-53s4asfm.js} +13 -13
  33. package/dist/{chunk-tsk2bxs3.js.map → chunk-53s4asfm.js.map} +3 -3
  34. package/dist/{chunk-8r8cw2xw.js → chunk-63te770v.js} +40 -5
  35. package/dist/{chunk-8r8cw2xw.js.map → chunk-63te770v.js.map} +3 -3
  36. package/dist/{chunk-vj723r3p.js → chunk-6rzdtvn5.js} +1 -1
  37. package/dist/{chunk-5yny9z0e.js → chunk-6zrpewy4.js} +1 -1
  38. package/dist/{chunk-nvamz5dv.js → chunk-86459jn0.js} +294 -22
  39. package/dist/chunk-86459jn0.js.map +22 -0
  40. package/dist/{chunk-p6r5g77m.js → chunk-8kv2sez0.js} +1 -1
  41. package/dist/{chunk-k41ajtz7.js → chunk-9k3hgv8h.js} +1 -1
  42. package/dist/{chunk-jdwx7vzs.js → chunk-avppcgt6.js} +7 -7
  43. package/dist/{chunk-jdwx7vzs.js.map → chunk-avppcgt6.js.map} +1 -1
  44. package/dist/{chunk-2gpr6jsz.js → chunk-b1bqvhpe.js} +2 -2
  45. package/dist/{chunk-pv1yad91.js → chunk-b5dp7gaf.js} +17 -90
  46. package/dist/{chunk-pv1yad91.js.map → chunk-b5dp7gaf.js.map} +5 -6
  47. package/dist/{chunk-1vffvydj.js → chunk-be9vm29v.js} +6 -6
  48. package/dist/chunk-be9vm29v.js.map +10 -0
  49. package/dist/{chunk-wxhdnwh4.js → chunk-bh8bwma3.js} +1 -1
  50. package/dist/{chunk-0psb6kdw.js → chunk-btcsywk4.js} +12 -16
  51. package/dist/{chunk-0psb6kdw.js.map → chunk-btcsywk4.js.map} +3 -3
  52. package/dist/{chunk-0fygrz37.js → chunk-exrfbt2d.js} +1 -1
  53. package/dist/{chunk-c3zd880g.js → chunk-f6kyyeef.js} +2 -2
  54. package/dist/{chunk-q4qzq0ea.js → chunk-hb9xcx9c.js} +3 -3
  55. package/dist/{chunk-4pycxcj3.js → chunk-hz8m927w.js} +6 -9
  56. package/dist/{chunk-4pycxcj3.js.map → chunk-hz8m927w.js.map} +3 -3
  57. package/dist/{chunk-btpe64e4.js → chunk-m9w0artv.js} +1 -1
  58. package/dist/{chunk-45tw90k7.js → chunk-md9jbrm9.js} +1 -1
  59. package/dist/{chunk-dhmj53mx.js → chunk-nbywkytr.js} +3 -3
  60. package/dist/{chunk-t358gywy.js → chunk-p7vj8xgb.js} +8 -8
  61. package/dist/{chunk-2s6sydsh.js → chunk-qzyemcj7.js} +294 -4
  62. package/dist/chunk-qzyemcj7.js.map +13 -0
  63. package/dist/{chunk-r65yscvk.js → chunk-r82k4z7z.js} +131 -13
  64. package/dist/chunk-r82k4z7z.js.map +13 -0
  65. package/dist/{chunk-aebzcsxr.js → chunk-raq0x6bd.js} +11 -9
  66. package/dist/{chunk-aebzcsxr.js.map → chunk-raq0x6bd.js.map} +4 -4
  67. package/dist/{chunk-hsv43v58.js → chunk-rgzed50y.js} +77 -22
  68. package/dist/chunk-rgzed50y.js.map +22 -0
  69. package/dist/{chunk-yvef9xwc.js → chunk-rjqm6ksg.js} +7 -7
  70. package/dist/{chunk-f2b5e2gf.js → chunk-sb0shdvv.js} +18 -3
  71. package/dist/{chunk-f2b5e2gf.js.map → chunk-sb0shdvv.js.map} +3 -3
  72. package/dist/{chunk-dcgrzj1t.js → chunk-xt6qx9sh.js} +1 -1
  73. package/dist/{chunk-mx84y05q.js → chunk-y2ygpd0m.js} +2 -2
  74. package/dist/{chunk-yxss87qa.js → chunk-yn947ck3.js} +1 -1
  75. package/dist/datasets/builders.d.ts +18 -5
  76. package/dist/datasets/builders.d.ts.map +1 -1
  77. package/dist/datasets/changes.d.ts +18 -0
  78. package/dist/datasets/changes.d.ts.map +1 -0
  79. package/dist/datasets/index.d.ts +3 -1
  80. package/dist/datasets/index.d.ts.map +1 -1
  81. package/dist/datasets/types.d.ts +2 -0
  82. package/dist/datasets/types.d.ts.map +1 -1
  83. package/dist/datasets/validation.d.ts.map +1 -1
  84. package/dist/events/index.js +2 -2
  85. package/dist/http/api-routes.d.ts.map +1 -1
  86. package/dist/http/index.js +1 -1
  87. package/dist/index.d.ts +6 -6
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +64 -58
  90. package/dist/index.js.map +1 -1
  91. package/dist/json.d.ts +8 -1
  92. package/dist/json.d.ts.map +1 -1
  93. package/dist/lake-storage/definition-updates.d.ts.map +1 -1
  94. package/dist/lake-storage/in-memory.d.ts +15 -1
  95. package/dist/lake-storage/in-memory.d.ts.map +1 -1
  96. package/dist/lake-storage/index.d.ts +3 -1
  97. package/dist/lake-storage/index.d.ts.map +1 -1
  98. package/dist/lake-storage/index.js +15 -7
  99. package/dist/lake-storage/index.js.map +1 -1
  100. package/dist/lake-storage/merge-validation.d.ts +10 -0
  101. package/dist/lake-storage/merge-validation.d.ts.map +1 -0
  102. package/dist/lake-storage/merge.d.ts +32 -0
  103. package/dist/lake-storage/merge.d.ts.map +1 -0
  104. package/dist/lake-storage/types.d.ts +3 -1
  105. package/dist/lake-storage/types.d.ts.map +1 -1
  106. package/dist/logging/index.js +3 -3
  107. package/dist/logging/types.js +2 -2
  108. package/dist/materialization/index.js +2 -2
  109. package/dist/materializer/index.js +15 -15
  110. package/dist/materializer/index.js.map +1 -1
  111. package/dist/objects/index.js +17 -17
  112. package/dist/objects/query/index.js +8 -8
  113. package/dist/ontology/index.js +5 -5
  114. package/dist/ontology/internal.js +7 -6
  115. package/dist/ontology/internal.js.map +3 -3
  116. package/dist/ontology/validation/normalize.d.ts.map +1 -1
  117. package/dist/projections/internal.js +4 -4
  118. package/dist/runtime/sixb.d.ts.map +1 -1
  119. package/dist/storage/index.d.ts +1 -1
  120. package/dist/storage/index.d.ts.map +1 -1
  121. package/dist/storage/index.js +16 -16
  122. package/dist/storage/index.js.map +1 -1
  123. package/dist/storage/ontology/provider.js +4 -4
  124. package/dist/storage/sync-runs/index.d.ts +1 -1
  125. package/dist/storage/sync-runs/index.d.ts.map +1 -1
  126. package/dist/storage/sync-runs/types.d.ts +7 -3
  127. package/dist/storage/sync-runs/types.d.ts.map +1 -1
  128. package/dist/storage/types.d.ts +1 -1
  129. package/dist/storage/types.d.ts.map +1 -1
  130. package/dist/storage/workflow-runs/in-memory.d.ts.map +1 -1
  131. package/dist/storage/workflow-runs/types.d.ts +2 -0
  132. package/dist/storage/workflow-runs/types.d.ts.map +1 -1
  133. package/dist/syncs/builders.d.ts +11 -3
  134. package/dist/syncs/builders.d.ts.map +1 -1
  135. package/dist/syncs/index.d.ts +1 -1
  136. package/dist/syncs/index.d.ts.map +1 -1
  137. package/dist/syncs/types.d.ts +24 -19
  138. package/dist/syncs/types.d.ts.map +1 -1
  139. package/dist/syncs/validation.d.ts +14 -0
  140. package/dist/syncs/validation.d.ts.map +1 -0
  141. package/dist/testing/index.d.ts +1 -0
  142. package/dist/testing/index.d.ts.map +1 -1
  143. package/dist/testing/index.js +837 -583
  144. package/dist/testing/index.js.map +5 -4
  145. package/dist/testing/lake-merge-storage-contract.d.ts +7 -0
  146. package/dist/testing/lake-merge-storage-contract.d.ts.map +1 -0
  147. package/dist/testing/lake-storage-contract.d.ts.map +1 -1
  148. package/dist/workflows/index.js +23 -23
  149. package/package.json +1 -1
  150. package/src/agents/adapters.ts +1 -9
  151. package/src/agents/builders.ts +53 -3
  152. package/src/agents/errors.ts +22 -0
  153. package/src/agents/index.ts +22 -2
  154. package/src/agents/prompt.ts +2 -2
  155. package/src/agents/tool-definition.ts +93 -0
  156. package/src/agents/types.ts +101 -0
  157. package/src/agents/validation.ts +287 -3
  158. package/src/blob-storage/validation.ts +2 -10
  159. package/src/datasets/builders.ts +49 -5
  160. package/src/datasets/changes.ts +13 -0
  161. package/src/datasets/index.ts +3 -0
  162. package/src/datasets/types.ts +5 -0
  163. package/src/datasets/validation.ts +53 -11
  164. package/src/http/api-routes.ts +39 -4
  165. package/src/index.ts +18 -1
  166. package/src/json.ts +11 -4
  167. package/src/lake-storage/definition-updates.ts +21 -0
  168. package/src/lake-storage/in-memory.ts +284 -2
  169. package/src/lake-storage/index.ts +14 -0
  170. package/src/lake-storage/merge-validation.ts +117 -0
  171. package/src/lake-storage/merge.ts +35 -0
  172. package/src/lake-storage/types.ts +3 -1
  173. package/src/ontology/json-schema.ts +2 -1
  174. package/src/ontology/validation/normalize.ts +9 -15
  175. package/src/runtime/sixb.ts +16 -1
  176. package/src/storage/index.ts +1 -0
  177. package/src/storage/sync-runs/in-memory.ts +2 -2
  178. package/src/storage/sync-runs/index.ts +1 -0
  179. package/src/storage/sync-runs/types.ts +9 -4
  180. package/src/storage/types.ts +1 -0
  181. package/src/storage/workflow-runs/in-memory.ts +2 -0
  182. package/src/storage/workflow-runs/types.ts +2 -0
  183. package/src/syncs/builders.ts +31 -13
  184. package/src/syncs/index.ts +1 -0
  185. package/src/syncs/types.ts +44 -18
  186. package/src/syncs/validation.ts +77 -0
  187. package/src/testing/index.ts +4 -0
  188. package/src/testing/lake-merge-storage-contract.ts +302 -0
  189. package/src/testing/lake-storage-contract.ts +23 -0
  190. package/src/workflows/snapshot.ts +1 -10
  191. package/dist/chunk-1vffvydj.js.map +0 -10
  192. package/dist/chunk-2s6sydsh.js.map +0 -12
  193. package/dist/chunk-hsv43v58.js.map +0 -21
  194. package/dist/chunk-nvamz5dv.js.map +0 -21
  195. package/dist/chunk-r65yscvk.js.map +0 -11
  196. /package/dist/{chunk-590dejbp.js.map → chunk-4hwxq5e6.js.map} +0 -0
  197. /package/dist/{chunk-wj01s5hr.js.map → chunk-4tvj3ate.js.map} +0 -0
  198. /package/dist/{chunk-vj723r3p.js.map → chunk-6rzdtvn5.js.map} +0 -0
  199. /package/dist/{chunk-5yny9z0e.js.map → chunk-6zrpewy4.js.map} +0 -0
  200. /package/dist/{chunk-p6r5g77m.js.map → chunk-8kv2sez0.js.map} +0 -0
  201. /package/dist/{chunk-k41ajtz7.js.map → chunk-9k3hgv8h.js.map} +0 -0
  202. /package/dist/{chunk-2gpr6jsz.js.map → chunk-b1bqvhpe.js.map} +0 -0
  203. /package/dist/{chunk-wxhdnwh4.js.map → chunk-bh8bwma3.js.map} +0 -0
  204. /package/dist/{chunk-0fygrz37.js.map → chunk-exrfbt2d.js.map} +0 -0
  205. /package/dist/{chunk-c3zd880g.js.map → chunk-f6kyyeef.js.map} +0 -0
  206. /package/dist/{chunk-q4qzq0ea.js.map → chunk-hb9xcx9c.js.map} +0 -0
  207. /package/dist/{chunk-btpe64e4.js.map → chunk-m9w0artv.js.map} +0 -0
  208. /package/dist/{chunk-45tw90k7.js.map → chunk-md9jbrm9.js.map} +0 -0
  209. /package/dist/{chunk-dhmj53mx.js.map → chunk-nbywkytr.js.map} +0 -0
  210. /package/dist/{chunk-t358gywy.js.map → chunk-p7vj8xgb.js.map} +0 -0
  211. /package/dist/{chunk-yvef9xwc.js.map → chunk-rjqm6ksg.js.map} +0 -0
  212. /package/dist/{chunk-dcgrzj1t.js.map → chunk-xt6qx9sh.js.map} +0 -0
  213. /package/dist/{chunk-mx84y05q.js.map → chunk-y2ygpd0m.js.map} +0 -0
  214. /package/dist/{chunk-yxss87qa.js.map → chunk-yn947ck3.js.map} +0 -0
@@ -159,6 +159,13 @@ export const SIXB_API_ROUTES: readonly SixbApiRoute[] = [
159
159
  accessToken: true,
160
160
  agentApi: true,
161
161
  },
162
+ {
163
+ operationId: "listObjectLinks",
164
+ method: "GET",
165
+ path: "/api/objects/:objectTypeId/:objectId/links",
166
+ accessToken: true,
167
+ agentApi: true,
168
+ },
162
169
  // The four write routes. Bearer-capable because they now enforce scoped authorization
163
170
  // (`edit:object` / `append:telemetry`), which is exactly the precondition this table's rule states.
164
171
  // NOT agent-proxied: letting an agent turn write objects on its own is a separate decision, and
@@ -252,6 +259,20 @@ export const SIXB_API_ROUTES: readonly SixbApiRoute[] = [
252
259
  accessToken: true,
253
260
  agentApi: true,
254
261
  },
262
+ {
263
+ operationId: "uploadFileRaw",
264
+ method: "POST",
265
+ path: "/api/files",
266
+ accessToken: true,
267
+ agentApi: true,
268
+ },
269
+ {
270
+ operationId: "listActionRuns",
271
+ method: "GET",
272
+ path: "/api/action-runs",
273
+ accessToken: true,
274
+ agentApi: true,
275
+ },
255
276
  {
256
277
  operationId: "getActionRun",
257
278
  method: "GET",
@@ -278,21 +299,35 @@ export const SIXB_API_ROUTES: readonly SixbApiRoute[] = [
278
299
  method: "GET",
279
300
  path: "/api/workflows",
280
301
  accessToken: true,
281
- agentApi: false,
302
+ agentApi: true,
282
303
  },
283
304
  {
284
305
  operationId: "getWorkflow",
285
306
  method: "GET",
286
307
  path: "/api/workflows/:workflowId",
287
308
  accessToken: true,
288
- agentApi: false,
309
+ agentApi: true,
289
310
  },
290
311
  {
291
312
  operationId: "requestWorkflowRun",
292
313
  method: "POST",
293
314
  path: "/api/workflows/:workflowId/runs",
294
315
  accessToken: true,
295
- agentApi: false,
316
+ agentApi: true,
317
+ },
318
+ {
319
+ operationId: "listWorkflowRuns",
320
+ method: "GET",
321
+ path: "/api/workflow-runs",
322
+ accessToken: true,
323
+ agentApi: true,
324
+ },
325
+ {
326
+ operationId: "getWorkflowRun",
327
+ method: "GET",
328
+ path: "/api/workflow-runs/:runId",
329
+ accessToken: true,
330
+ agentApi: true,
296
331
  },
297
332
  {
298
333
  operationId: "cancelWorkflowRun",
@@ -313,7 +348,7 @@ export const SIXB_API_ROUTES: readonly SixbApiRoute[] = [
313
348
  method: "GET",
314
349
  path: "/api/workflow-runs/:runId/files/content",
315
350
  accessToken: true,
316
- agentApi: false,
351
+ agentApi: true,
317
352
  },
318
353
  {
319
354
  operationId: "headWorkflowRunFileContent",
package/src/index.ts CHANGED
@@ -140,9 +140,12 @@ export type {
140
140
  DatasetColumnTypeOf,
141
141
  DatasetColumnUnionOf,
142
142
  DatasetDefinition,
143
+ DatasetPrimaryKey,
143
144
  DatasetSchema,
145
+ MergeChange,
144
146
  } from "./datasets"
145
147
  export {
148
+ change,
146
149
  col,
147
150
  DatasetValidationError,
148
151
  defineDataset,
@@ -154,7 +157,7 @@ export {
154
157
 
155
158
  export type { Broker, BrokerCursor } from "./broker"
156
159
  export { InMemoryBroker } from "./broker"
157
- export type { JsonValue } from "./json"
160
+ export type { JsonValue, ReadonlyJsonValue } from "./json"
158
161
  export {
159
162
  assertJsonValue,
160
163
  cloneJsonValue,
@@ -629,6 +632,7 @@ export type {
629
632
  RequestSyncRunInput,
630
633
  SyncBuilder,
631
634
  SyncDefinition,
635
+ SyncMode,
632
636
  SyncReadBuilder,
633
637
  SyncReadContext,
634
638
  SyncReadHandler,
@@ -867,10 +871,21 @@ export type {
867
871
  AgentTextPart,
868
872
  AgentToolCallPart,
869
873
  AgentToolCallState,
874
+ AgentToolDefinition,
875
+ AgentToolDescriptionBuilder,
876
+ AgentToolHandler,
877
+ AgentToolHandlerResult,
878
+ AgentToolInputBuilder,
879
+ AgentToolInputSchema,
880
+ AgentToolRunBuilder,
881
+ AgentToolRunContext,
882
+ AgentToolRunInfo,
870
883
  AgentUiMessage,
871
884
  AgentUiMessagePart,
872
885
  AgentUiToolPart,
873
886
  DefineAgentConfig,
887
+ InferAgentToolInput,
888
+ InferAgentToolInputSchema,
874
889
  RequestAgentRunInput,
875
890
  RequestAgentRunResult,
876
891
  } from "./agents"
@@ -878,9 +893,11 @@ export {
878
893
  AGENT_REASONING_LEVELS,
879
894
  AgentDefinitionError,
880
895
  AgentRequestError,
896
+ AgentToolPublicError,
881
897
  agentContext,
882
898
  agentContextIdentity,
883
899
  defineAgent,
900
+ defineAgentTool,
884
901
  isAgentDefinition,
885
902
  MAX_AGENT_APP_STATE_ENTRY_BYTES,
886
903
  MAX_AGENT_APP_STATE_TOTAL_BYTES,
package/src/json.ts CHANGED
@@ -3,6 +3,13 @@ export type JsonObject = { [key: string]: JsonValue }
3
3
  export type JsonArray = JsonValue[]
4
4
  export type JsonValue = JsonPrimitive | JsonObject | JsonArray
5
5
 
6
+ /** JSON-compatible input that also accepts readonly containers from typed application code. */
7
+ export type ReadonlyJsonObject = {
8
+ readonly [key: string]: ReadonlyJsonValue
9
+ }
10
+ export type ReadonlyJsonArray = readonly ReadonlyJsonValue[]
11
+ export type ReadonlyJsonValue = JsonPrimitive | ReadonlyJsonObject | ReadonlyJsonArray
12
+
6
13
  export function isJsonValue(value: unknown): value is JsonValue {
7
14
  return getInvalidJsonValueReason(value) === undefined
8
15
  }
@@ -14,8 +21,8 @@ export function assertJsonValue(value: unknown, label = "value"): asserts value
14
21
  }
15
22
  }
16
23
 
17
- export function cloneJsonValue(value: JsonValue): JsonValue {
18
- assertJsonValue(value)
24
+ export function cloneJsonValue(value: ReadonlyJsonValue, label = "value"): JsonValue {
25
+ assertJsonValue(value, label)
19
26
  return JSON.parse(JSON.stringify(value)) as JsonValue
20
27
  }
21
28
 
@@ -74,7 +81,7 @@ export function getInvalidJsonValueReason(
74
81
  return undefined
75
82
  }
76
83
 
77
- if (isPlainObject(value)) {
84
+ if (isPlainRecord(value)) {
78
85
  if (seen.has(value)) {
79
86
  return `${label} contains a circular reference`
80
87
  }
@@ -101,7 +108,7 @@ export function getInvalidJsonValueReason(
101
108
  return `${label} is ${describeNonJsonValue(value)}`
102
109
  }
103
110
 
104
- function isPlainObject(value: unknown): value is Record<string, unknown> {
111
+ export function isPlainRecord(value: unknown): value is Record<string, unknown> {
105
112
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
106
113
  return false
107
114
  }
@@ -36,6 +36,7 @@ export function planDatasetDefinitionUpdate(
36
36
  requested: DatasetDefinition
37
37
  ): DatasetDefinitionUpdatePlan {
38
38
  assertSameDataset(existing, requested)
39
+ assertPrimaryKeyUnchanged(existing, requested)
39
40
 
40
41
  const addedColumns = getAddedColumnsAfterValidatingExistingColumns(existing, requested)
41
42
  assertAddedColumnsAreNullable(requested.id, addedColumns)
@@ -98,6 +99,24 @@ function assertSameDataset(existing: DatasetDefinition, requested: DatasetDefini
98
99
  }
99
100
  }
100
101
 
102
+ function assertPrimaryKeyUnchanged(
103
+ existing: DatasetDefinition,
104
+ requested: DatasetDefinition
105
+ ): void {
106
+ const existingPrimaryKey = existing.primaryKey
107
+ const requestedPrimaryKey = requested.primaryKey
108
+ const unchanged =
109
+ existingPrimaryKey === undefined || requestedPrimaryKey === undefined
110
+ ? existingPrimaryKey === requestedPrimaryKey
111
+ : areDefinitionValuesEqual(existingPrimaryKey, requestedPrimaryKey)
112
+
113
+ if (!unchanged) {
114
+ throw new LakeStorageError(
115
+ `[SixbLake] Dataset '${requested.id}' cannot be redefined with an incompatible primaryKey. Primary keys are immutable.`
116
+ )
117
+ }
118
+ }
119
+
101
120
  function getAddedColumnsAfterValidatingExistingColumns(
102
121
  existing: DatasetDefinition,
103
122
  requested: DatasetDefinition
@@ -193,6 +212,7 @@ function mergeDatasetDefinition(
193
212
  )
194
213
 
195
214
  const columns = [...cloneColumns(existing.schema.columns), ...cloneColumns(addedColumns)]
215
+ const primaryKey = existing.primaryKey
196
216
  const partitionBy = requested.partitionBy ?? existing.partitionBy
197
217
  const description = requested.description ?? existing.description
198
218
 
@@ -200,6 +220,7 @@ function mergeDatasetDefinition(
200
220
  kind: "dataset",
201
221
  id: requested.id,
202
222
  schema: { columns },
223
+ ...(primaryKey !== undefined ? { primaryKey: structuredClone(primaryKey) } : {}),
203
224
  ...(partitionBy !== undefined ? { partitionBy: [...partitionBy] } : {}),
204
225
  ...(description !== undefined ? { description } : {}),
205
226
  }
@@ -1,8 +1,20 @@
1
1
  import { randomUUID } from "node:crypto"
2
- import type { DatasetDefinition, DatasetSchema } from "../datasets"
2
+ import type { DatasetDefinition, DatasetSchema, MergeChange } from "../datasets"
3
3
  import { getDatasetRowValidationError } from "../datasets/validation"
4
4
  import { mergeStrictDatasetDefinition } from "./definition-updates"
5
5
  import { LakeStorageError } from "./errors"
6
+ import type {
7
+ BeginDatasetMergeInput,
8
+ CommitDatasetMergeInput,
9
+ DatasetMergeCommitResult,
10
+ LakeMergeSession,
11
+ } from "./merge"
12
+ import {
13
+ cloneDatasetMergeChange,
14
+ encodeDatasetPrimaryKey,
15
+ getDatasetMergeChangeValidationError,
16
+ getDatasetPrimaryKeyColumns,
17
+ } from "./merge-validation"
6
18
  import type {
7
19
  BeginDatasetWriteInput,
8
20
  CommitDatasetWriteInput,
@@ -74,6 +86,7 @@ function errorMessage(error: unknown): string {
74
86
 
75
87
  class InMemoryLakeWriteSession implements LakeWriteSession {
76
88
  private readonly rows: DatasetRow[] = []
89
+ private readonly primaryKeys = new Set<string>()
77
90
  private closed = false
78
91
 
79
92
  constructor(
@@ -84,13 +97,30 @@ class InMemoryLakeWriteSession implements LakeWriteSession {
84
97
  async writeRows(rows: Iterable<DatasetRow> | AsyncIterable<DatasetRow>): Promise<void> {
85
98
  this.assertOpen()
86
99
 
100
+ const stagedRows: DatasetRow[] = []
101
+ const stagedPrimaryKeys = new Set<string>()
87
102
  for await (const row of rows) {
88
103
  const validationError = getDatasetRowValidationError(row, this.input.dataset)
89
104
  if (validationError) {
90
105
  throw new LakeStorageError(`[LakeStorage] ${validationError}`)
91
106
  }
92
107
 
93
- this.rows.push(cloneRow(row))
108
+ if (getDatasetPrimaryKeyColumns(this.input.dataset) !== null) {
109
+ const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, row)
110
+ if (this.primaryKeys.has(primaryKey) || stagedPrimaryKeys.has(primaryKey)) {
111
+ throw new LakeStorageError(
112
+ `[LakeStorage] Dataset '${this.input.dataset.id}' write contains duplicate primary key ${primaryKey}.`
113
+ )
114
+ }
115
+ stagedPrimaryKeys.add(primaryKey)
116
+ }
117
+
118
+ stagedRows.push(cloneRow(row))
119
+ }
120
+
121
+ this.rows.push(...stagedRows)
122
+ for (const primaryKey of stagedPrimaryKeys) {
123
+ this.primaryKeys.add(primaryKey)
94
124
  }
95
125
  }
96
126
 
@@ -116,11 +146,63 @@ class InMemoryLakeWriteSession implements LakeWriteSession {
116
146
  }
117
147
  }
118
148
 
149
+ class InMemoryLakeMergeSession implements LakeMergeSession {
150
+ private readonly changes: MergeChange<DatasetRow, DatasetRow>[] = []
151
+ private closed = false
152
+
153
+ constructor(
154
+ private readonly storage: InMemoryLakeStorage,
155
+ private readonly input: BeginDatasetMergeInput,
156
+ private readonly baseVersionId: string | null
157
+ ) {}
158
+
159
+ async writeChanges(
160
+ changes:
161
+ | Iterable<MergeChange<DatasetRow, DatasetRow>>
162
+ | AsyncIterable<MergeChange<DatasetRow, DatasetRow>>
163
+ ): Promise<void> {
164
+ this.assertOpen()
165
+
166
+ const stagedChanges: MergeChange<DatasetRow, DatasetRow>[] = []
167
+ for await (const change of changes) {
168
+ const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset)
169
+ if (validationError) {
170
+ throw new LakeStorageError(`[LakeStorage] ${validationError}`)
171
+ }
172
+ stagedChanges.push(cloneDatasetMergeChange(change))
173
+ }
174
+ this.changes.push(...stagedChanges)
175
+ }
176
+
177
+ async commit(input?: CommitDatasetMergeInput): Promise<DatasetMergeCommitResult> {
178
+ this.assertOpen()
179
+ this.closed = true
180
+ return this.storage.commitMerge({
181
+ merge: this.input,
182
+ baseVersionId: this.baseVersionId,
183
+ changes: this.changes,
184
+ commit: input,
185
+ })
186
+ }
187
+
188
+ async abort(): Promise<void> {
189
+ this.closed = true
190
+ this.changes.length = 0
191
+ }
192
+
193
+ private assertOpen(): void {
194
+ if (this.closed) {
195
+ throw new LakeStorageError("[LakeStorage] Merge session is already closed")
196
+ }
197
+ }
198
+ }
199
+
119
200
  export class InMemoryLakeStorage implements LakeStorage {
120
201
  private readonly datasets = new Map<string, DatasetDefinition>()
121
202
  private readonly versionsByDataset = new Map<string, DatasetVersion[]>()
122
203
  private readonly rowsByVersionId = new Map<string, readonly DatasetRow[]>()
123
204
  private readonly latestVersionIdByDataset = new Map<string, string>()
205
+ private readonly commitLocks = new Map<string, Promise<void>>()
124
206
 
125
207
  async createDataset(definition: DatasetDefinition): Promise<DatasetDefinition> {
126
208
  assertDatasetId(definition.id)
@@ -220,6 +302,36 @@ export class InMemoryLakeStorage implements LakeStorage {
220
302
  })
221
303
  }
222
304
 
305
+ async beginMerge(input: BeginDatasetMergeInput): Promise<LakeMergeSession> {
306
+ const definition = await this.getDataset(input.dataset.id)
307
+ if (!definition) {
308
+ throw new LakeStorageError(`[LakeStorage] Unknown dataset '${input.dataset.id}'`)
309
+ }
310
+ if (getDatasetPrimaryKeyColumns(definition) === null) {
311
+ throw new LakeStorageError(
312
+ `[LakeStorage] Dataset '${definition.id}' must define a primaryKey before it can be merged.`
313
+ )
314
+ }
315
+
316
+ const latestVersion = await this.getLatestVersion(definition.id)
317
+ if (
318
+ input.expectedLatestVersionId !== undefined &&
319
+ latestVersion?.versionId !== input.expectedLatestVersionId
320
+ ) {
321
+ throw new LakeStorageError(
322
+ `[LakeStorage] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'`
323
+ )
324
+ }
325
+ return new InMemoryLakeMergeSession(
326
+ this,
327
+ {
328
+ ...input,
329
+ dataset: definition,
330
+ },
331
+ latestVersion?.versionId ?? null
332
+ )
333
+ }
334
+
223
335
  async getLatestVersion(datasetId: string): Promise<DatasetVersion | null> {
224
336
  const latestVersionId = this.latestVersionIdByDataset.get(datasetId)
225
337
  if (!latestVersionId) {
@@ -267,6 +379,90 @@ export class InMemoryLakeStorage implements LakeStorage {
267
379
  write: BeginDatasetWriteInput
268
380
  rows: readonly DatasetRow[]
269
381
  commit?: CommitDatasetWriteInput
382
+ }): Promise<DatasetWriteCommitResult> {
383
+ return this.withDatasetCommitLock(options.write.dataset.id, () =>
384
+ this.commitWriteUnlocked(options)
385
+ )
386
+ }
387
+
388
+ async commitMerge(options: {
389
+ merge: BeginDatasetMergeInput
390
+ baseVersionId: string | null
391
+ changes: readonly MergeChange<DatasetRow, DatasetRow>[]
392
+ commit?: CommitDatasetMergeInput
393
+ }): Promise<DatasetMergeCommitResult> {
394
+ return this.withDatasetCommitLock(options.merge.dataset.id, async () => {
395
+ const definition = this.datasets.get(options.merge.dataset.id)
396
+ if (!definition) {
397
+ throw new LakeStorageError(`[LakeStorage] Unknown dataset '${options.merge.dataset.id}'`)
398
+ }
399
+
400
+ const latestVersion = await this.getLatestVersion(options.merge.dataset.id)
401
+ const actualVersionId = latestVersion?.versionId ?? null
402
+ if (actualVersionId !== options.baseVersionId) {
403
+ throw new LakeStorageError(
404
+ `[LakeStorage] Optimistic merge commit failed for dataset '${options.merge.dataset.id}': expected latest version '${options.baseVersionId ?? "none"}', found '${actualVersionId ?? "none"}'`
405
+ )
406
+ }
407
+
408
+ const previousRows = this.rowsByVersionId.get(latestVersion?.versionId ?? "") ?? []
409
+ const previousByKey = this.rowsByPrimaryKey(previousRows, definition)
410
+ const finalChanges = new Map<string, MergeChange<DatasetRow, DatasetRow>>()
411
+ for (const change of options.changes) {
412
+ const value = change.kind === "upsert" ? change.row : change.key
413
+ finalChanges.set(encodeDatasetPrimaryKey(definition, value), change)
414
+ }
415
+
416
+ const nextByKey = new Map(previousByKey)
417
+ for (const [primaryKey, change] of finalChanges) {
418
+ if (change.kind === "upsert") {
419
+ nextByKey.set(primaryKey, cloneRow(change.row))
420
+ } else {
421
+ nextByKey.delete(primaryKey)
422
+ }
423
+ }
424
+
425
+ if (this.sameKeyedRowContent(previousByKey, nextByKey, definition.schema)) {
426
+ return {
427
+ outcome: "unchanged",
428
+ version: latestVersion ? cloneDatasetVersion(latestVersion) : null,
429
+ }
430
+ }
431
+
432
+ const visibleRows = [...nextByKey.values()]
433
+ const versionId = `ver_${randomUUID()}`
434
+ const version: DatasetVersion = {
435
+ datasetId: options.merge.dataset.id,
436
+ versionId,
437
+ parentVersionId: latestVersion?.versionId,
438
+ mode: "merge",
439
+ createdAt: new Date(),
440
+ schema: definition.schema,
441
+ producer: options.merge.producer ? structuredClone(options.merge.producer) : undefined,
442
+ inputs: options.merge.inputs ? structuredClone(options.merge.inputs) : undefined,
443
+ rowCount: visibleRows.length,
444
+ sizeBytes: new TextEncoder().encode(
445
+ visibleRows.map((row) => JSON.stringify(row)).join("\n")
446
+ ).byteLength,
447
+ }
448
+
449
+ const versions = this.versionsByDataset.get(options.merge.dataset.id) ?? []
450
+ versions.push(version)
451
+ this.versionsByDataset.set(options.merge.dataset.id, versions)
452
+ this.rowsByVersionId.set(
453
+ versionId,
454
+ visibleRows.map((row) => cloneRow(row))
455
+ )
456
+ this.latestVersionIdByDataset.set(options.merge.dataset.id, versionId)
457
+
458
+ return { outcome: "created", version: cloneDatasetVersion(version) }
459
+ })
460
+ }
461
+
462
+ private async commitWriteUnlocked(options: {
463
+ write: BeginDatasetWriteInput
464
+ rows: readonly DatasetRow[]
465
+ commit?: CommitDatasetWriteInput
270
466
  }): Promise<DatasetWriteCommitResult> {
271
467
  const definition = this.datasets.get(options.write.dataset.id)
272
468
  if (!definition) {
@@ -285,6 +481,8 @@ export class InMemoryLakeStorage implements LakeStorage {
285
481
  }
286
482
  }
287
483
 
484
+ this.assertKeyedWriteIsUnique(mode, options.rows, latestVersion, definition)
485
+
288
486
  // Content-identical snapshots and empty appends reuse the latest version
289
487
  // instead of creating a new one.
290
488
  if (
@@ -345,4 +543,88 @@ export class InMemoryLakeStorage implements LakeStorage {
345
543
  previousRows.map((row) => rowContentKey(row, schema))
346
544
  )
347
545
  }
546
+
547
+ private assertKeyedWriteIsUnique(
548
+ mode: DatasetWriteMode,
549
+ rows: readonly DatasetRow[],
550
+ latestVersion: DatasetVersion | null,
551
+ definition: DatasetDefinition
552
+ ): void {
553
+ if (getDatasetPrimaryKeyColumns(definition) === null) {
554
+ return
555
+ }
556
+
557
+ const stagedByKey = this.rowsByPrimaryKey(rows, definition)
558
+ if (mode !== "append" || latestVersion === null) {
559
+ return
560
+ }
561
+
562
+ const previousRows = this.rowsByVersionId.get(latestVersion.versionId) ?? []
563
+ const previousByKey = this.rowsByPrimaryKey(previousRows, definition)
564
+ for (const primaryKey of stagedByKey.keys()) {
565
+ if (previousByKey.has(primaryKey)) {
566
+ throw new LakeStorageError(
567
+ `[LakeStorage] Dataset '${definition.id}' append contains duplicate primary key ${primaryKey}.`
568
+ )
569
+ }
570
+ }
571
+ }
572
+
573
+ private rowsByPrimaryKey(
574
+ rows: readonly DatasetRow[],
575
+ definition: DatasetDefinition
576
+ ): Map<string, DatasetRow> {
577
+ const byKey = new Map<string, DatasetRow>()
578
+ for (const row of rows) {
579
+ const primaryKey = encodeDatasetPrimaryKey(definition, row)
580
+ if (byKey.has(primaryKey)) {
581
+ throw new LakeStorageError(
582
+ `[LakeStorage] Dataset '${definition.id}' contains duplicate primary key ${primaryKey}.`
583
+ )
584
+ }
585
+ byKey.set(primaryKey, row)
586
+ }
587
+ return byKey
588
+ }
589
+
590
+ private sameKeyedRowContent(
591
+ left: ReadonlyMap<string, DatasetRow>,
592
+ right: ReadonlyMap<string, DatasetRow>,
593
+ schema: DatasetSchema
594
+ ): boolean {
595
+ if (left.size !== right.size) {
596
+ return false
597
+ }
598
+ for (const [primaryKey, leftRow] of left) {
599
+ const rightRow = right.get(primaryKey)
600
+ if (
601
+ rightRow === undefined ||
602
+ rowContentKey(leftRow, schema) !== rowContentKey(rightRow, schema)
603
+ ) {
604
+ return false
605
+ }
606
+ }
607
+ return true
608
+ }
609
+
610
+ private async withDatasetCommitLock<T>(datasetId: string, run: () => Promise<T>): Promise<T> {
611
+ const previous = this.commitLocks.get(datasetId) ?? Promise.resolve()
612
+ const ready = previous.catch(() => {})
613
+ let release!: () => void
614
+ const next = new Promise<void>((resolve) => {
615
+ release = resolve
616
+ })
617
+ const current = ready.then(() => next)
618
+ this.commitLocks.set(datasetId, current)
619
+
620
+ await ready
621
+ try {
622
+ return await run()
623
+ } finally {
624
+ release()
625
+ if (this.commitLocks.get(datasetId) === current) {
626
+ this.commitLocks.delete(datasetId)
627
+ }
628
+ }
629
+ }
348
630
  }
@@ -2,7 +2,9 @@ export type {
2
2
  DatasetColumnDefinition,
3
3
  DatasetColumnType,
4
4
  DatasetDefinition,
5
+ DatasetPrimaryKey,
5
6
  DatasetSchema,
7
+ MergeChange,
6
8
  } from "../datasets"
7
9
  export type { JsonValue } from "../json"
8
10
  export type {
@@ -17,6 +19,18 @@ export {
17
19
  } from "./definition-updates"
18
20
  export { LakeStorageError } from "./errors"
19
21
  export { InMemoryLakeStorage } from "./in-memory"
22
+ export type {
23
+ BeginDatasetMergeInput,
24
+ CommitDatasetMergeInput,
25
+ DatasetMergeCommitResult,
26
+ LakeMergeSession,
27
+ } from "./merge"
28
+ export {
29
+ cloneDatasetMergeChange,
30
+ encodeDatasetPrimaryKey,
31
+ getDatasetMergeChangeValidationError,
32
+ getDatasetPrimaryKeyColumns,
33
+ } from "./merge-validation"
20
34
  export type {
21
35
  ExecuteSqlTransformInput,
22
36
  LakeSqlExecutor,