deepline 0.3.227 → 0.3.229

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 (34) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/play-data-plane/contracts.ts +12 -0
  3. package/dist/bundling-sources/shared_libs/play-data-plane/index.ts +1 -0
  4. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-column-storage.ts +252 -0
  5. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +29 -8
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +250 -37
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +29 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-projection-diagnostics.ts +43 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +6 -1
  11. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +380 -183
  12. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +17 -28
  13. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -0
  14. package/dist/bundling-sources/shared_libs/plays/dataset.ts +16 -2
  15. package/dist/bundling-sources/shared_libs/plays/execution-pipeline.ts +72 -0
  16. package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +1 -3
  17. package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +9 -13
  18. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +28 -31
  19. package/dist/cli/index.js +1 -1
  20. package/dist/cli/index.mjs +1 -1
  21. package/dist/{compiler-manifest-Dpd2hEet.d.mts → compiler-manifest-n8ZtPv2h.d.mts} +31 -21
  22. package/dist/{compiler-manifest-Dpd2hEet.d.ts → compiler-manifest-n8ZtPv2h.d.ts} +31 -21
  23. package/dist/index.d.mts +2 -2
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +6 -1
  26. package/dist/index.mjs +6 -1
  27. package/dist/install-integrity.json +4 -2
  28. package/dist/plays/bundle-play-file.d.mts +2 -2
  29. package/dist/plays/bundle-play-file.d.ts +2 -2
  30. package/dist/release.d.mts +1 -1
  31. package/dist/release.d.ts +1 -1
  32. package/dist/release.js +1 -1
  33. package/dist/release.mjs +1 -1
  34. package/package.json +1 -1
@@ -24,6 +24,10 @@ import {
24
24
  physicalSheetColumnProjections,
25
25
  type PhysicalSheetColumnProjection,
26
26
  } from '../play-data-plane/sheet-contract';
27
+ import {
28
+ RuntimeSheetColumns,
29
+ type RuntimeSheetColumnTypes,
30
+ } from '../play-data-plane/sheet-column-storage';
27
31
  import { RUNTIME_SHEET_CELL_META_COLUMN } from '../play-data-plane/runtime-sheet-storage-contract';
28
32
  import {
29
33
  createRuntimeSheetOrm,
@@ -2925,7 +2929,7 @@ function missingOutputCellSql(
2925
2929
  const quoted = `${tableAlias}.${quoteIdentifier(column.sqlName)}`;
2926
2930
  const staleAt = `${tableAlias}._cell_meta -> ${quoteLiteral(column.fieldName)} -> 'staleAt'`;
2927
2931
  const staleAtMs = `(CASE WHEN jsonb_typeof(${staleAt}) = 'number' THEN (${staleAt})::text::double precision ELSE NULL END)`;
2928
- return `(${quoted} IS NULL OR ${quoted} = 'null'::jsonb OR ${quoted} = '""'::jsonb OR (${staleAtMs} IS NOT NULL AND ${staleAtMs} <= extract(epoch from now()) * 1000))`;
2932
+ return `(${quoted} IS NULL OR to_jsonb(${quoted}) = 'null'::jsonb OR to_jsonb(${quoted}) = '""'::jsonb OR (${staleAtMs} IS NOT NULL AND ${staleAtMs} <= extract(epoch from now()) * 1000))`;
2929
2933
  })
2930
2934
  .join(' OR ');
2931
2935
  }
@@ -2941,7 +2945,7 @@ function changedPatchedCellSql(
2941
2945
  return projections
2942
2946
  .map((column) => {
2943
2947
  const quoted = `${tableAlias}.${quoteIdentifier(column.sqlName)}`;
2944
- return `(${patchAlias} ? ${quoteLiteral(column.fieldName)} AND ${quoted} IS DISTINCT FROM ${patchAlias} -> ${quoteLiteral(column.fieldName)})`;
2948
+ return `(${patchAlias} ? ${quoteLiteral(column.fieldName)} AND coalesce(to_jsonb(${quoted}), 'null'::jsonb) IS DISTINCT FROM ${patchAlias} -> ${quoteLiteral(column.fieldName)})`;
2945
2949
  })
2946
2950
  .join(' OR ');
2947
2951
  }
@@ -3331,12 +3335,6 @@ async function writeRuntimeRows(
3331
3335
  physicalColumns.length > 0
3332
3336
  ? `, ${physicalColumns.map(quoteIdentifier).join(', ')}`
3333
3337
  : '';
3334
- const physicalInsertValuesSql =
3335
- physicalColumns.length > 0
3336
- ? `, ${physicalColumnProjections
3337
- .map((column) => `payload -> ${quoteLiteral(column.fieldName)}`)
3338
- .join(', ')}`
3339
- : '';
3340
3338
 
3341
3339
  const rowsToWrite = input.rows.filter(
3342
3340
  (row) => row && typeof row === 'object' && !Array.isArray(row),
@@ -3349,10 +3347,8 @@ async function writeRuntimeRows(
3349
3347
  input.tableNamespace,
3350
3348
  );
3351
3349
 
3352
- // Build write entries first so we know chunk count up front. Single-chunk
3353
- // writes skip the BEGIN/COMMIT pair entirely (the mega-CTE is atomic at
3354
- // statement level), and the per-chunk mega-CTE folds: starting-index lookup,
3355
- // sheet insert, and summary upsert into one round-trip.
3350
+ // Keep every chunk's row insert and summary update in the same transaction
3351
+ // as column inference, so a concurrent writer cannot invalidate its schema.
3356
3352
  const rowEntries =
3357
3353
  input.mode === 'upsert'
3358
3354
  ? Array.from(
@@ -3365,7 +3361,11 @@ async function writeRuntimeRows(
3365
3361
  return uniqueRows;
3366
3362
  }, new Map<string, Record<string, unknown>>())
3367
3363
  .entries(),
3368
- ).map(([key, row], inputIndex) => ({ key, row, inputIndex }))
3364
+ ).map(([key, row], inputIndex) => ({
3365
+ key,
3366
+ row: sanitizePostgresJsonValue(row),
3367
+ inputIndex,
3368
+ }))
3369
3369
  : rowsToWrite.map((row, index) => ({
3370
3370
  key: buildAppendedRowKey({
3371
3371
  row,
@@ -3373,7 +3373,7 @@ async function writeRuntimeRows(
3373
3373
  idempotencyKey: input.idempotencyKey,
3374
3374
  ordinal: index,
3375
3375
  }),
3376
- row,
3376
+ row: sanitizePostgresJsonValue(row),
3377
3377
  // For append/replace modes the starting offset is computed in SQL
3378
3378
  // as `coalesce(max(_input_index), -1) + 1 + (ord - 1)`; we only
3379
3379
  // pass the per-chunk ordinal here so the SQL can add it to the
@@ -3382,12 +3382,47 @@ async function writeRuntimeRows(
3382
3382
  }));
3383
3383
 
3384
3384
  const chunks = chunkValues(rowEntries, DIRECT_POSTGRES_BATCH_SIZE);
3385
- const needsTransaction = input.mode === 'replace' || chunks.length > 1;
3385
+ // Type evolution and the row mutation share one transaction and schema lock.
3386
+ const needsTransaction = true;
3386
3387
 
3387
3388
  return await withRuntimePostgres(session, async (client, transaction) => {
3388
3389
  const managesTransaction = needsTransaction && !transaction.active;
3389
3390
  if (managesTransaction) await client.query('BEGIN');
3390
3391
  try {
3392
+ const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
3393
+ schema: session.postgres.schema,
3394
+ table: session.postgres.sheetTable,
3395
+ columns: physicalColumnProjections,
3396
+ rows: async () => {
3397
+ if (input.mode !== 'upsert')
3398
+ return rowEntries.map((entry) => entry.row);
3399
+ const existing = await client.query<{ _key: string }>(
3400
+ `SELECT _key FROM ${sheetTable(session)} WHERE _key = ANY($1::text[])`,
3401
+ [rowEntries.map((entry) => entry.key)],
3402
+ );
3403
+ const existingKeys = new Set(existing.rows.map((row) => row._key));
3404
+ return rowEntries
3405
+ .filter((entry) => !existingKeys.has(entry.key))
3406
+ .map((entry) => entry.row);
3407
+ },
3408
+ });
3409
+ const physicalInsertValuesSql =
3410
+ physicalColumns.length > 0
3411
+ ? `, ${physicalColumnProjections
3412
+ .map((column) => {
3413
+ const incoming = RuntimeSheetColumns.jsonValueSql(
3414
+ `input_rows.payload -> ${quoteLiteral(column.fieldName)}`,
3415
+ sheetTable(session),
3416
+ column.sqlName,
3417
+ columnTypes,
3418
+ );
3419
+ return input.mode === 'upsert'
3420
+ ? `CASE WHEN existing_input._key IS NOT NULL THEN existing_input.${quoteIdentifier(column.sqlName)} ELSE ${incoming} END`
3421
+ : incoming;
3422
+ })
3423
+ .join(', ')}`
3424
+ : '';
3425
+
3391
3426
  if (input.mode === 'replace') {
3392
3427
  // Collapse 4 cleanup statements into one CTE-shaped query. Postgres
3393
3428
  // executes data-modifying CTEs against the same snapshot, but each
@@ -3438,8 +3473,9 @@ async function writeRuntimeRows(
3438
3473
  input.mode === 'upsert'
3439
3474
  ? `inserted_rows AS (
3440
3475
  INSERT INTO ${sheetTable(session)} (_key, _status, _run_id, _input_index, _created_at${physicalInsertColumnsSql})
3441
- SELECT _key, 'pending', $4::text, _input_index, now()${physicalInsertValuesSql}
3476
+ SELECT input_rows._key, 'pending', $4::text, input_rows._input_index, now()${physicalInsertValuesSql}
3442
3477
  FROM input_rows
3478
+ LEFT JOIN ${sheetTable(session)} AS existing_input ON existing_input._key = input_rows._key
3443
3479
  ON CONFLICT (_key) DO UPDATE SET
3444
3480
  _status = 'pending',
3445
3481
  _run_id = EXCLUDED._run_id,
@@ -3764,8 +3800,9 @@ async function prepareRuntimeSheetDatasetRows(
3764
3800
  ),
3765
3801
  inserted_rows AS (
3766
3802
  INSERT INTO ${sheetTable(session)} (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id, _attempt_expires_at, _attempt_seq, _write_version, _writer_run_id, _created_at${input.physicalInsertColumnsSql})
3767
- SELECT _key, 'pending', $4::text, _input_index, $7::text, $8::text, $9::timestamptz, $10::integer, $12::bigint, $4::text, now()${input.physicalInsertValuesSql}
3803
+ SELECT input_rows._key, 'pending', $4::text, input_rows._input_index, $7::text, $8::text, $9::timestamptz, $10::integer, $12::bigint, $4::text, now()${input.physicalInsertValuesSql}
3768
3804
  FROM input_rows
3805
+ LEFT JOIN ${sheetTable(session)} AS existing_input ON existing_input._key = input_rows._key
3769
3806
  ON CONFLICT (_key) DO UPDATE SET
3770
3807
  _status = 'pending',
3771
3808
  _run_id = EXCLUDED._run_id,
@@ -4111,9 +4148,10 @@ async function prepareNetNewRuntimeSheetDatasetRows(
4111
4148
  INSERT INTO ${sheetTable(session)}
4112
4149
  (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id,
4113
4150
  _attempt_expires_at, _attempt_seq, _write_version, _writer_run_id, _created_at${input.physicalInsertColumnsSql})
4114
- SELECT _key, 'pending', $4::text, _input_index, $5::text, $6::text,
4151
+ SELECT input_rows._key, 'pending', $4::text, input_rows._input_index, $5::text, $6::text,
4115
4152
  $7::timestamptz, $8::integer, $9::bigint, $4::text, now()${input.physicalInsertValuesSql}
4116
4153
  FROM input_rows
4154
+ LEFT JOIN ${sheetTable(session)} AS existing_input ON existing_input._key = input_rows._key
4117
4155
  ON CONFLICT (_key) DO NOTHING
4118
4156
  RETURNING _key
4119
4157
  ),
@@ -6591,7 +6629,7 @@ export async function startRuntimeSheetDataset(
6591
6629
  derivePlayRowIdentity(cleanedRow, input.tableNamespace);
6592
6630
  if (key && !uniqueRows.has(key)) {
6593
6631
  uniqueRows.set(key, {
6594
- row: cleanedRow,
6632
+ row: sanitizePostgresJsonValue(cleanedRow),
6595
6633
  inputIndex: normalizeRuntimeMapInputIndex(
6596
6634
  row[MAP_ROW_OUTCOME_RUNTIME_FIELDS.inputIndex],
6597
6635
  ),
@@ -6635,12 +6673,6 @@ export async function startRuntimeSheetDataset(
6635
6673
  physicalColumns.length > 0
6636
6674
  ? `, ${physicalColumns.map(quoteIdentifier).join(', ')}`
6637
6675
  : '';
6638
- const physicalInsertValuesSql =
6639
- physicalColumns.length > 0
6640
- ? `, ${physicalColumns
6641
- .map((column) => `payload -> ${quoteLiteral(column)}`)
6642
- .join(', ')}`
6643
- : '';
6644
6676
  const outputPhysicalColumns = outputPhysicalSheetColumnProjections(
6645
6677
  input.sheetContract,
6646
6678
  );
@@ -6650,15 +6682,6 @@ export async function startRuntimeSheetDataset(
6650
6682
  const physicalRefreshColumns = physicalColumns.filter(
6651
6683
  (column) => !outputPhysicalColumnNames.has(column),
6652
6684
  );
6653
- const physicalRefreshSetSql =
6654
- physicalRefreshColumns.length > 0
6655
- ? `, ${physicalRefreshColumns
6656
- .map(
6657
- (column) =>
6658
- `${quoteIdentifier(column)} = input_rows.payload -> ${quoteLiteral(column)}`,
6659
- )
6660
- .join(', ')}`
6661
- : '';
6662
6685
  const physicalUpsertSetSql =
6663
6686
  physicalColumns.length > 0
6664
6687
  ? `, ${physicalColumns
@@ -6705,6 +6728,77 @@ export async function startRuntimeSheetDataset(
6705
6728
  session,
6706
6729
  input.writeVersion,
6707
6730
  );
6731
+ const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
6732
+ schema: session.postgres.schema,
6733
+ table: session.postgres.sheetTable,
6734
+ columns: physicalSheetColumnProjections(input.sheetContract).map(
6735
+ (column) => ({ ...column, fieldName: column.sqlName }),
6736
+ ),
6737
+ rows: async () => {
6738
+ const existing = await client.query<{
6739
+ _key: string;
6740
+ _status: string;
6741
+ _run_id: string | null;
6742
+ row_write_stamp: string | number;
6743
+ _input_index: string | number | null;
6744
+ }>(
6745
+ `SELECT _key, _status, _run_id, _write_version AS row_write_stamp, _input_index
6746
+ FROM ${sheetTable(session)} WHERE _key = ANY($1::text[]) FOR UPDATE`,
6747
+ [rowEntries.map((entry) => entry.key)],
6748
+ );
6749
+ const current = new Map(existing.rows.map((row) => [row._key, row]));
6750
+ return rowEntries.flatMap((entry) => {
6751
+ const row = current.get(entry.key);
6752
+ if (!row) return [entry.row];
6753
+ if (input.mode === 'net_new') return [];
6754
+ if (row._status === 'stale') return [entry.row];
6755
+ if (input.force !== true) return [];
6756
+ const version = Number(row.row_write_stamp);
6757
+ const refreshes =
6758
+ version < writeVersion ||
6759
+ (version === writeVersion &&
6760
+ (row._status !== 'enriched' ||
6761
+ row._run_id !== input.runId ||
6762
+ row._input_index === null ||
6763
+ Number(row._input_index) !== entry.inputIndex));
6764
+ if (!refreshes) return [];
6765
+ return [
6766
+ Object.fromEntries(
6767
+ Object.entries(entry.row).filter(
6768
+ ([field]) => !outputPhysicalColumnNames.has(field),
6769
+ ),
6770
+ ),
6771
+ ];
6772
+ });
6773
+ },
6774
+ });
6775
+ const physicalInsertValuesSql =
6776
+ physicalColumns.length > 0
6777
+ ? `, ${physicalColumns
6778
+ .map((column) => {
6779
+ const incoming = RuntimeSheetColumns.jsonValueSql(
6780
+ `input_rows.payload -> ${quoteLiteral(column)}`,
6781
+ sheetTable(session),
6782
+ column,
6783
+ columnTypes,
6784
+ );
6785
+ const preserveExisting =
6786
+ input.mode === 'net_new'
6787
+ ? 'existing_input._key IS NOT NULL'
6788
+ : "existing_input._key IS NOT NULL AND existing_input._status <> 'stale'";
6789
+ return `CASE WHEN ${preserveExisting} THEN existing_input.${quoteIdentifier(column)} ELSE ${incoming} END`;
6790
+ })
6791
+ .join(', ')}`
6792
+ : '';
6793
+ const physicalRefreshSetSql =
6794
+ physicalRefreshColumns.length > 0
6795
+ ? `, ${physicalRefreshColumns
6796
+ .map(
6797
+ (column) =>
6798
+ `${quoteIdentifier(column)} = ${RuntimeSheetColumns.jsonValueSql(`input_rows.payload -> ${quoteLiteral(column)}`, sheetTable(session), column, columnTypes)}`,
6799
+ )
6800
+ .join(', ')}`
6801
+ : '';
6708
6802
  timings.push({
6709
6803
  phase: 'attempt_expiry_mint',
6710
6804
  ms: Date.now() - attemptExpiryStartedAt,
@@ -7105,9 +7199,28 @@ export async function releaseRuntimeWorkReceipts(
7105
7199
  );
7106
7200
  }
7107
7201
 
7202
+ function physicalRuntimeSheetUpdateSql(
7203
+ session: RuntimePostgresSession,
7204
+ projections: readonly PhysicalSheetColumnProjection[],
7205
+ columnTypes: RuntimeSheetColumnTypes,
7206
+ ): string {
7207
+ return projections.length > 0
7208
+ ? `,\n ${projections
7209
+ .map((column) => {
7210
+ const quoted = quoteIdentifier(column.sqlName);
7211
+ const literal = quoteLiteral(column.fieldName);
7212
+ return `${quoted} = CASE
7213
+ WHEN coalesce(updates.data_patch, '{}'::jsonb) ? ${literal}
7214
+ THEN ${RuntimeSheetColumns.jsonValueSql(`updates.data_patch -> ${literal}`, sheetTable(session), column.sqlName, columnTypes)}
7215
+ ELSE target.${quoted}
7216
+ END`;
7217
+ })
7218
+ .join(',\n ')}`
7219
+ : '';
7220
+ }
7221
+
7108
7222
  type CompleteRuntimeMapRowChunksInput = {
7109
7223
  chunks: RuntimePreparedCompletedRow[][];
7110
- physicalUpdateSetSql: string;
7111
7224
  physicalColumnProjections: PhysicalSheetColumnProjection[];
7112
7225
  runId: string;
7113
7226
  attemptId: string | null;
@@ -7385,12 +7498,7 @@ async function completeRuntimeMapRowChunks(
7385
7498
  OR coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $10::text
7386
7499
  )
7387
7500
  )`;
7388
- const { rows } = await client.query<{
7389
- updated: number;
7390
- matched_keys: string[];
7391
- fenced_rows: unknown;
7392
- }>(
7393
- `WITH updates AS (
7501
+ const matchingUpdatesSql = `WITH mutation_context AS (SELECT $6::text AS play_name, $7::text AS table_namespace, $11::timestamptz AS attempt_expires_at, $12::integer AS attempt_seq), updates AS (
7394
7502
  SELECT key_values._key,
7395
7503
  input_index_values.input_index,
7396
7504
  data_values.data_patch,
@@ -7414,23 +7522,8 @@ async function completeRuntimeMapRowChunks(
7414
7522
  FROM updates
7415
7523
  JOIN ${sheetTable(session)} AS target
7416
7524
  ON target._key = updates._key
7417
- ),
7418
- applied_rows AS (
7419
- UPDATE ${sheetTable(session)} AS target
7420
- SET _status = 'enriched',
7421
- _run_id = $5::text,
7422
- _writer_run_id = $5::text,
7423
- _write_version = COALESCE($13::bigint, target._write_version),
7424
- _error = NULL,
7425
- _attempt_id = $9::text,
7426
- _attempt_owner_run_id = $10::text,
7427
- _attempt_expires_at = $11::timestamptz,
7428
- _attempt_seq = $12::integer,
7429
- _updated_at = now(),
7430
- _version = ${nextRuntimeSheetVersionExpression(session)},
7431
- _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${input.physicalUpdateSetSql}
7432
- FROM matched_updates AS updates
7433
- WHERE target._key = updates.matched_key
7525
+ )`;
7526
+ const writableRowsSql = `target._key = updates.matched_key
7434
7527
  AND ($13::bigint IS NULL OR target._write_version = $13::bigint)
7435
7528
  AND ($13::bigint IS NOT NULL OR target._status <> 'enriched')
7436
7529
  AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
@@ -7471,7 +7564,63 @@ async function completeRuntimeMapRowChunks(
7471
7564
  FROM unnest($8::text[]) AS field_values(field)
7472
7565
  WHERE coalesce(target._cell_meta -> field_values.field ->> 'status', '') <> 'completed'
7473
7566
  )
7474
- )
7567
+ )`;
7568
+ const mutationParams = [
7569
+ chunkKeys,
7570
+ chunkInputIndexes,
7571
+ chunkDataPatches,
7572
+ chunkCellMetaPatches,
7573
+ input.runId,
7574
+ input.normalizedPlayName,
7575
+ input.normalizedTableNamespace,
7576
+ [...new Set(input.outputFields)],
7577
+ input.attemptId,
7578
+ input.attemptOwnerRunId,
7579
+ input.attemptExpiresAt,
7580
+ input.attemptSeq,
7581
+ input.writeVersion,
7582
+ ];
7583
+ const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
7584
+ schema: session.postgres.schema,
7585
+ table: session.postgres.sheetTable,
7586
+ columns: input.physicalColumnProjections,
7587
+ rows: async () => {
7588
+ const accepted = await client.query<{
7589
+ data_patch: Record<string, unknown>;
7590
+ }>(
7591
+ `${matchingUpdatesSql}
7592
+ SELECT updates.data_patch
7593
+ FROM matched_updates AS updates
7594
+ JOIN ${sheetTable(session)} AS target ON target._key = updates.matched_key
7595
+ WHERE ${writableRowsSql}
7596
+ FOR UPDATE OF target`,
7597
+ mutationParams,
7598
+ );
7599
+ return accepted.rows.map((row) => row.data_patch);
7600
+ },
7601
+ });
7602
+ const { rows } = await client.query<{
7603
+ updated: number;
7604
+ matched_keys: string[];
7605
+ fenced_rows: unknown;
7606
+ }>(
7607
+ `${matchingUpdatesSql},
7608
+ applied_rows AS (
7609
+ UPDATE ${sheetTable(session)} AS target
7610
+ SET _status = 'enriched',
7611
+ _run_id = $5::text,
7612
+ _writer_run_id = $5::text,
7613
+ _write_version = COALESCE($13::bigint, target._write_version),
7614
+ _error = NULL,
7615
+ _attempt_id = $9::text,
7616
+ _attempt_owner_run_id = $10::text,
7617
+ _attempt_expires_at = $11::timestamptz,
7618
+ _attempt_seq = $12::integer,
7619
+ _updated_at = now(),
7620
+ _version = ${nextRuntimeSheetVersionExpression(session)},
7621
+ _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${physicalRuntimeSheetUpdateSql(session, input.physicalColumnProjections, columnTypes)}
7622
+ FROM matched_updates AS updates
7623
+ WHERE ${writableRowsSql}
7475
7624
  RETURNING target._key, updates.prev_status, updates.prev_cell_meta
7476
7625
  ),
7477
7626
  applied_count AS (
@@ -7595,21 +7744,7 @@ async function completeRuntimeMapRowChunks(
7595
7744
  coalesce((SELECT jsonb_agg(jsonb_build_object('key', _key, 'reason', reason, 'rowVersionStamp', row_write_version)) FROM fenced_rows), '[]'::jsonb) AS fenced_rows,
7596
7745
  (SELECT count(*)::int FROM summary_delta) AS summary_delta_count,
7597
7746
  (SELECT count(*)::int FROM column_delta) AS column_delta_count`,
7598
- [
7599
- chunkKeys,
7600
- chunkInputIndexes,
7601
- chunkDataPatches,
7602
- chunkCellMetaPatches,
7603
- input.runId,
7604
- input.normalizedPlayName,
7605
- input.normalizedTableNamespace,
7606
- [...new Set(input.outputFields)],
7607
- input.attemptId,
7608
- input.attemptOwnerRunId,
7609
- input.attemptExpiresAt,
7610
- input.attemptSeq,
7611
- input.writeVersion,
7612
- ],
7747
+ mutationParams,
7613
7748
  );
7614
7749
  const matchedKeys = new Set(rows[0]?.matched_keys ?? []);
7615
7750
  const chunkFencedRows = parseRuntimeMapRowsFencedRows(rows[0]?.fenced_rows);
@@ -7663,12 +7798,7 @@ async function completeRuntimeMapRowChunksWithInputIndexRepair(
7663
7798
  '$11::timestamptz',
7664
7799
  '$12',
7665
7800
  );
7666
- const { rows } = await client.query<{
7667
- _key?: string;
7668
- fenced_rows?: unknown;
7669
- updated?: number;
7670
- }>(
7671
- `WITH updates AS (
7801
+ const matchingUpdatesSql = `WITH mutation_context AS (SELECT $6::text AS play_name, $7::text AS table_namespace, $11::timestamptz AS attempt_expires_at, $12::integer AS attempt_seq), updates AS (
7672
7802
  SELECT key_values._key,
7673
7803
  input_index_values.input_index,
7674
7804
  data_values.data_patch,
@@ -7696,24 +7826,8 @@ async function completeRuntimeMapRowChunksWithInputIndexRepair(
7696
7826
  AND target._input_index = updates.input_index
7697
7827
  )
7698
7828
  ORDER BY target._key, (target._key = updates._key) DESC
7699
- ),
7700
- applied_rows AS (
7701
- UPDATE ${sheetTable(session)} AS target
7702
- SET _status = 'enriched',
7703
- _run_id = $5::text,
7704
- _writer_run_id = $5::text,
7705
- _write_version = COALESCE($13::bigint, target._write_version),
7706
- _error = NULL,
7707
- _attempt_id = $9::text,
7708
- _attempt_owner_run_id = $10::text,
7709
- _attempt_expires_at = $11::timestamptz,
7710
- _attempt_seq = $12::integer,
7711
- _updated_at = now(),
7712
- _version = ${nextRuntimeSheetVersionExpression(session)},
7713
- _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${input.physicalUpdateSetSql}
7714
- FROM matched_updates AS updates, ${sheetTable(session)} AS prev
7715
- WHERE target._key = updates.matched_key
7716
- AND prev._key = target._key
7829
+ )`;
7830
+ const writableRowsSql = `target._key = updates.matched_key
7717
7831
  AND ($13::bigint IS NULL OR target._write_version = $13::bigint)
7718
7832
  AND ($13::bigint IS NOT NULL OR target._status <> 'enriched')
7719
7833
  AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
@@ -7749,7 +7863,64 @@ async function completeRuntimeMapRowChunksWithInputIndexRepair(
7749
7863
  FROM unnest($8::text[]) AS field_values(field)
7750
7864
  WHERE coalesce(target._cell_meta -> field_values.field ->> 'status', '') <> 'completed'
7751
7865
  )
7752
- )
7866
+ )`;
7867
+ const mutationParams = [
7868
+ chunkKeys,
7869
+ chunkInputIndexes,
7870
+ chunkDataPatches,
7871
+ chunkCellMetaPatches,
7872
+ input.runId,
7873
+ input.normalizedPlayName,
7874
+ input.normalizedTableNamespace,
7875
+ [...new Set(input.outputFields)],
7876
+ input.attemptId,
7877
+ input.attemptOwnerRunId,
7878
+ input.attemptExpiresAt,
7879
+ input.attemptSeq,
7880
+ input.writeVersion,
7881
+ ];
7882
+ const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
7883
+ schema: session.postgres.schema,
7884
+ table: session.postgres.sheetTable,
7885
+ columns: input.physicalColumnProjections,
7886
+ rows: async () => {
7887
+ const accepted = await client.query<{
7888
+ data_patch: Record<string, unknown>;
7889
+ }>(
7890
+ `${matchingUpdatesSql}
7891
+ SELECT updates.data_patch
7892
+ FROM matched_updates AS updates
7893
+ JOIN ${sheetTable(session)} AS target ON target._key = updates.matched_key
7894
+ WHERE ${writableRowsSql}
7895
+ FOR UPDATE OF target`,
7896
+ mutationParams,
7897
+ );
7898
+ return accepted.rows.map((row) => row.data_patch);
7899
+ },
7900
+ });
7901
+ const { rows } = await client.query<{
7902
+ _key?: string;
7903
+ fenced_rows?: unknown;
7904
+ updated?: number;
7905
+ }>(
7906
+ `${matchingUpdatesSql},
7907
+ applied_rows AS (
7908
+ UPDATE ${sheetTable(session)} AS target
7909
+ SET _status = 'enriched',
7910
+ _run_id = $5::text,
7911
+ _writer_run_id = $5::text,
7912
+ _write_version = COALESCE($13::bigint, target._write_version),
7913
+ _error = NULL,
7914
+ _attempt_id = $9::text,
7915
+ _attempt_owner_run_id = $10::text,
7916
+ _attempt_expires_at = $11::timestamptz,
7917
+ _attempt_seq = $12::integer,
7918
+ _updated_at = now(),
7919
+ _version = ${nextRuntimeSheetVersionExpression(session)},
7920
+ _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${physicalRuntimeSheetUpdateSql(session, input.physicalColumnProjections, columnTypes)}
7921
+ FROM matched_updates AS updates, ${sheetTable(session)} AS prev
7922
+ WHERE ${writableRowsSql}
7923
+ AND prev._key = target._key
7753
7924
  RETURNING target._key, prev._status AS prev_status, prev._cell_meta AS prev_cell_meta
7754
7925
  ),
7755
7926
  applied_count AS (
@@ -7868,21 +8039,7 @@ async function completeRuntimeMapRowChunksWithInputIndexRepair(
7868
8039
  SELECT
7869
8040
  (SELECT count(*)::int FROM applied_rows) AS updated,
7870
8041
  coalesce((SELECT jsonb_agg(jsonb_build_object('key', _key, 'reason', reason, 'rowVersionStamp', row_write_version)) FROM fenced_rows), '[]'::jsonb) AS fenced_rows`,
7871
- [
7872
- chunkKeys,
7873
- chunkInputIndexes,
7874
- chunkDataPatches,
7875
- chunkCellMetaPatches,
7876
- input.runId,
7877
- input.normalizedPlayName,
7878
- input.normalizedTableNamespace,
7879
- [...new Set(input.outputFields)],
7880
- input.attemptId,
7881
- input.attemptOwnerRunId,
7882
- input.attemptExpiresAt,
7883
- input.attemptSeq,
7884
- input.writeVersion,
7885
- ],
8042
+ mutationParams,
7886
8043
  );
7887
8044
  updated += Number(rows[0]?.updated ?? 0);
7888
8045
  fencedRows.push(...parseRuntimeMapRowsFencedRows(rows[0]?.fenced_rows));
@@ -7902,21 +8059,56 @@ async function insertMissingCompletedMapRowChunks(
7902
8059
  .map((column) => quoteIdentifier(column.sqlName))
7903
8060
  .join(', ')}`
7904
8061
  : '';
7905
- const physicalInsertValuesSql =
7906
- input.physicalColumnProjections.length > 0
7907
- ? `, ${input.physicalColumnProjections
7908
- .map(
7909
- (column) =>
7910
- `missing_rows.data_patch -> ${quoteLiteral(column.fieldName)}`,
7911
- )
7912
- .join(', ')}`
7913
- : '';
7914
8062
 
7915
8063
  for (const chunk of input.chunks) {
7916
8064
  const chunkKeys = chunk.map((row) => row.key);
7917
8065
  const chunkInputIndexes = chunk.map((row) => row.input_index);
7918
8066
  const chunkDataPatches = chunk.map((row) => row.data_patch_json);
7919
8067
  const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
8068
+ const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
8069
+ schema: session.postgres.schema,
8070
+ table: session.postgres.sheetTable,
8071
+ columns: input.physicalColumnProjections,
8072
+ rows: async () => {
8073
+ const accepted = await client.query<{
8074
+ data_patch: Record<string, unknown>;
8075
+ }>(
8076
+ `SELECT incoming.data_patch FROM jsonb_to_recordset($1::jsonb)
8077
+ AS incoming(key text, input_index bigint, data_patch jsonb)
8078
+ WHERE NOT EXISTS (
8079
+ SELECT 1 FROM ${sheetTable(session)} AS target
8080
+ WHERE target._key = incoming.key
8081
+ OR (incoming.input_index IS NOT NULL AND target._run_id = $2::text
8082
+ AND target._input_index = incoming.input_index)
8083
+ )`,
8084
+ [
8085
+ stringifyPostgresJson(
8086
+ chunk.map((row) => ({
8087
+ key: row.key,
8088
+ input_index: row.input_index,
8089
+ data_patch: JSON.parse(row.data_patch_json),
8090
+ })),
8091
+ ),
8092
+ input.runId,
8093
+ ],
8094
+ );
8095
+ return accepted.rows.map((row) => row.data_patch);
8096
+ },
8097
+ });
8098
+ const physicalInsertValuesSql =
8099
+ input.physicalColumnProjections.length > 0
8100
+ ? `, ${input.physicalColumnProjections
8101
+ .map((column) =>
8102
+ RuntimeSheetColumns.jsonValueSql(
8103
+ `missing_rows.data_patch -> ${quoteLiteral(column.fieldName)}`,
8104
+ sheetTable(session),
8105
+ column.sqlName,
8106
+ columnTypes,
8107
+ ),
8108
+ )
8109
+ .join(', ')}`
8110
+ : '';
8111
+
7920
8112
  const { rows } = await client.query<{ inserted: number }>(
7921
8113
  `WITH input_rows AS (
7922
8114
  SELECT key_values._key,
@@ -8068,7 +8260,6 @@ async function failRuntimeMapRowChunks(
8068
8260
  session: RuntimePostgresSession,
8069
8261
  input: {
8070
8262
  chunks: RuntimePreparedFailedRow[][];
8071
- physicalUpdateSetSql: string;
8072
8263
  physicalColumnProjections: PhysicalSheetColumnProjection[];
8073
8264
  forceTerminal?: boolean;
8074
8265
  runId: string;
@@ -8112,11 +8303,7 @@ async function failRuntimeMapRowChunks(
8112
8303
  const failedCellTotals = failedCellFields.map(
8113
8304
  (field) => failedCellCounts.get(field) ?? 0,
8114
8305
  );
8115
- const { rows } = await client.query<{
8116
- updated?: number;
8117
- fenced_rows?: unknown;
8118
- }>(
8119
- `WITH updates AS (
8306
+ const matchingUpdatesSql = `WITH mutation_context AS (SELECT $7::text AS play_name, $8::text AS table_namespace, $9::text[] AS failed_fields, $10::int[] AS failed_totals, $12::text AS attempt_owner_run_id, $13::timestamptz AS attempt_expires_at, $14::integer AS attempt_seq), updates AS (
8120
8307
  SELECT key_values._key,
8121
8308
  input_index_values.input_index,
8122
8309
  data_values.data_patch,
@@ -8148,25 +8335,9 @@ async function failRuntimeMapRowChunks(
8148
8335
  AND target._input_index = updates.input_index
8149
8336
  )
8150
8337
  ORDER BY target._key, (target._key = updates._key) DESC
8151
- ),
8152
- applied_rows AS (
8153
- UPDATE ${sheetTable(session)} AS target
8154
- SET _status = 'failed',
8155
- _run_id = $6::text,
8156
- _writer_run_id = $6::text,
8157
- _write_version = COALESCE($16::bigint, target._write_version),
8158
- _error = updates.error,
8159
- _attempt_id = $11::text,
8160
- _attempt_owner_run_id = $12::text,
8161
- _attempt_expires_at = $13::timestamptz,
8162
- _attempt_seq = $14::integer,
8163
- _updated_at = now(),
8164
- _version = ${nextRuntimeSheetVersionExpression(session)},
8165
- _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${input.physicalUpdateSetSql}
8166
- FROM matched_updates AS updates, ${sheetTable(session)} AS prev
8167
- WHERE target._key = updates.matched_key
8338
+ )`;
8339
+ const writableRowsSql = `target._key = updates.matched_key
8168
8340
  AND ($16::bigint IS NULL OR target._write_version = $16::bigint)
8169
- AND prev._key = target._key
8170
8341
  AND ($15::boolean OR target._status <> 'enriched')
8171
8342
  AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$13::timestamptz', '$14::integer', '$11::text', '$12::text')})
8172
8343
  AND (
@@ -8183,7 +8354,66 @@ async function failRuntimeMapRowChunks(
8183
8354
  AND target._run_id = $6::text
8184
8355
  AND target._status IN ('failed', 'enriched')
8185
8356
  AND updates.submitted_key = target._key
8186
- )
8357
+ )`;
8358
+ const mutationParams = [
8359
+ chunkKeys,
8360
+ chunkInputIndexes,
8361
+ chunkDataPatches,
8362
+ chunkCellMetaPatches,
8363
+ chunkErrors,
8364
+ input.runId,
8365
+ input.normalizedPlayName,
8366
+ input.normalizedTableNamespace,
8367
+ failedCellFields,
8368
+ failedCellTotals,
8369
+ input.attemptId,
8370
+ input.attemptOwnerRunId,
8371
+ input.attemptExpiresAt,
8372
+ input.attemptSeq,
8373
+ input.forceTerminal === true,
8374
+ input.writeVersion,
8375
+ ];
8376
+ const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
8377
+ schema: session.postgres.schema,
8378
+ table: session.postgres.sheetTable,
8379
+ columns: input.physicalColumnProjections,
8380
+ rows: async () => {
8381
+ const accepted = await client.query<{
8382
+ data_patch: Record<string, unknown>;
8383
+ }>(
8384
+ `${matchingUpdatesSql}
8385
+ SELECT updates.data_patch
8386
+ FROM matched_updates AS updates
8387
+ JOIN ${sheetTable(session)} AS target ON target._key = updates.matched_key
8388
+ WHERE ${writableRowsSql}
8389
+ FOR UPDATE OF target`,
8390
+ mutationParams,
8391
+ );
8392
+ return accepted.rows.map((row) => row.data_patch);
8393
+ },
8394
+ });
8395
+ const { rows } = await client.query<{
8396
+ updated?: number;
8397
+ fenced_rows?: unknown;
8398
+ }>(
8399
+ `${matchingUpdatesSql},
8400
+ applied_rows AS (
8401
+ UPDATE ${sheetTable(session)} AS target
8402
+ SET _status = 'failed',
8403
+ _run_id = $6::text,
8404
+ _writer_run_id = $6::text,
8405
+ _write_version = COALESCE($16::bigint, target._write_version),
8406
+ _error = updates.error,
8407
+ _attempt_id = $11::text,
8408
+ _attempt_owner_run_id = $12::text,
8409
+ _attempt_expires_at = $13::timestamptz,
8410
+ _attempt_seq = $14::integer,
8411
+ _updated_at = now(),
8412
+ _version = ${nextRuntimeSheetVersionExpression(session)},
8413
+ _cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${physicalRuntimeSheetUpdateSql(session, input.physicalColumnProjections, columnTypes)}
8414
+ FROM matched_updates AS updates, ${sheetTable(session)} AS prev
8415
+ WHERE ${writableRowsSql}
8416
+ AND prev._key = target._key
8187
8417
  RETURNING target._key, prev._status AS prev_status
8188
8418
  ),
8189
8419
  applied_count AS (
@@ -8280,24 +8510,7 @@ async function failRuntimeMapRowChunks(
8280
8510
  SELECT
8281
8511
  (SELECT count(*)::int FROM applied_rows) AS updated,
8282
8512
  coalesce((SELECT jsonb_agg(jsonb_build_object('key', _key, 'reason', reason, 'rowVersionStamp', row_write_version)) FROM fenced_rows), '[]'::jsonb) AS fenced_rows`,
8283
- [
8284
- chunkKeys,
8285
- chunkInputIndexes,
8286
- chunkDataPatches,
8287
- chunkCellMetaPatches,
8288
- chunkErrors,
8289
- input.runId,
8290
- input.normalizedPlayName,
8291
- input.normalizedTableNamespace,
8292
- failedCellFields,
8293
- failedCellTotals,
8294
- input.attemptId,
8295
- input.attemptOwnerRunId,
8296
- input.attemptExpiresAt,
8297
- input.attemptSeq,
8298
- input.forceTerminal === true,
8299
- input.writeVersion,
8300
- ],
8513
+ mutationParams,
8301
8514
  );
8302
8515
  const appliedCount = Number(rows[0]?.updated ?? 0);
8303
8516
  const chunkFencedRows = parseRuntimeMapRowsFencedRows(rows[0]?.fenced_rows);
@@ -8433,20 +8646,6 @@ export async function completeRuntimeMapRows(
8433
8646
  }
8434
8647
 
8435
8648
  const projections = physicalSheetColumnProjections(sheetContract);
8436
- const physicalUpdateSetSql =
8437
- projections.length > 0
8438
- ? `,\n ${projections
8439
- .map((column) => {
8440
- const quoted = quoteIdentifier(column.sqlName);
8441
- const literal = quoteLiteral(column.fieldName);
8442
- return `${quoted} = CASE
8443
- WHEN coalesce(updates.data_patch, '{}'::jsonb) ? ${literal}
8444
- THEN updates.data_patch -> ${literal}
8445
- ELSE target.${quoted}
8446
- END`;
8447
- })
8448
- .join(',\n ')}`
8449
- : '';
8450
8649
 
8451
8650
  const { completedRows, failedRows } = prepareRuntimeSheetRowTransitions({
8452
8651
  rows: uniqueRows.values(),
@@ -8455,7 +8654,7 @@ export async function completeRuntimeMapRows(
8455
8654
  });
8456
8655
  const chunks = chunkValues(completedRows, DIRECT_POSTGRES_BATCH_SIZE);
8457
8656
  const failedChunks = chunkValues(failedRows, DIRECT_POSTGRES_BATCH_SIZE);
8458
- const needsTransaction = chunks.length + failedChunks.length > 1;
8657
+ const needsTransaction = true;
8459
8658
  const outputFields = [...new Set(input.outputFields ?? [])];
8460
8659
  const attemptId = input.attemptId?.trim() || null;
8461
8660
  const attemptOwnerRunId =
@@ -8488,7 +8687,6 @@ export async function completeRuntimeMapRows(
8488
8687
  ? await (async () => {
8489
8688
  const chunkInput = {
8490
8689
  chunks,
8491
- physicalUpdateSetSql,
8492
8690
  physicalColumnProjections: projections,
8493
8691
  runId: input.runId,
8494
8692
  attemptId,
@@ -8522,7 +8720,6 @@ export async function completeRuntimeMapRows(
8522
8720
  failedRows.length > 0
8523
8721
  ? await failRuntimeMapRowChunks(client, session, {
8524
8722
  chunks: failedChunks,
8525
- physicalUpdateSetSql,
8526
8723
  physicalColumnProjections: projections,
8527
8724
  forceTerminal: input.forceFailedRows === true,
8528
8725
  runId: input.runId,