d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
import { getErrorMessage } from '@sim/utils/errors'
|
|
import { generateNKeysBetween } from '@sim/utils/fractional-indexing'
|
|
import type { ScriptMigration } from './types'
|
|
|
|
/**
|
|
* Rows written per `UPDATE … FROM unnest(...)` statement. The two unnest
|
|
* arrays are single bind parameters, so chunking only bounds per-statement
|
|
* work and wire-message size, not parameter count. Chunks share the one
|
|
* per-table transaction, so the table is still keyed atomically.
|
|
*/
|
|
const WRITE_CHUNK_SIZE = 5000
|
|
|
|
/**
|
|
* Backfills the `order_key` column on `user_table_rows`.
|
|
*
|
|
* Row ordering moved from the contiguous integer `position` to a fractional
|
|
* string `order_key` (O(1) insert/delete — no reshift/recompact). Each existing
|
|
* row gets a key derived from its current `position` order, so the fractional
|
|
* ordering matches the legacy ordering for rows that predate the column.
|
|
*
|
|
* Per-table-atomic: each table is keyed inside one transaction holding the same
|
|
* per-table advisory lock the app uses for inserts, so a concurrent insert
|
|
* can't interleave. Idempotent: tables already fully keyed are skipped; a table
|
|
* with any NULL key is fully re-keyed from `position` order (deterministic, so
|
|
* a re-run after a partial failure is safe). Per-table failures are isolated —
|
|
* remaining tables still run, then the migration throws so it stays pending and
|
|
* retries (only still-unkeyed tables) on the next upgrade.
|
|
*/
|
|
export const backfillTableOrderKeys: ScriptMigration = {
|
|
name: '0001_backfill_table_order_keys',
|
|
async up(sql) {
|
|
const pending = await sql<{ table_id: string }[]>`
|
|
SELECT DISTINCT table_id FROM user_table_rows WHERE order_key IS NULL
|
|
`
|
|
console.log(`order_key backfill — ${pending.length} table(s) with NULL order_key`)
|
|
|
|
const stats = { tablesKeyed: 0, rowsKeyed: 0, failed: 0 }
|
|
|
|
for (const { table_id: tableId } of pending) {
|
|
try {
|
|
const keyed = await sql.begin(async (tx) => {
|
|
await tx`
|
|
SELECT pg_advisory_xact_lock(hashtextextended(${`user_table_rows_pos:${tableId}`}, 0))
|
|
`
|
|
const rows = await tx<{ id: string }[]>`
|
|
SELECT id FROM user_table_rows
|
|
WHERE table_id = ${tableId}
|
|
ORDER BY position ASC, id ASC
|
|
`
|
|
if (rows.length === 0) return 0
|
|
const keys = generateNKeysBetween(null, null, rows.length)
|
|
|
|
for (let start = 0; start < rows.length; start += WRITE_CHUNK_SIZE) {
|
|
const chunk = rows.slice(start, start + WRITE_CHUNK_SIZE)
|
|
const ids = chunk.map((r) => r.id)
|
|
const chunkKeys = keys.slice(start, start + chunk.length)
|
|
await tx`
|
|
UPDATE user_table_rows AS t
|
|
SET order_key = v.order_key
|
|
FROM (
|
|
SELECT unnest(${ids}::text[]) AS id, unnest(${chunkKeys}::text[]) AS order_key
|
|
) AS v
|
|
WHERE t.id = v.id AND t.table_id = ${tableId}
|
|
`
|
|
}
|
|
return rows.length
|
|
})
|
|
stats.tablesKeyed += 1
|
|
stats.rowsKeyed += keyed
|
|
console.log(` ${tableId}: keyed ${keyed} rows`)
|
|
} catch (error) {
|
|
stats.failed += 1
|
|
console.error(` ${tableId}: FAILED — ${getErrorMessage(error)}`)
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`order_key backfill done — tables keyed: ${stats.tablesKeyed}, ` +
|
|
`rows keyed: ${stats.rowsKeyed}, failed: ${stats.failed}`
|
|
)
|
|
if (stats.failed > 0) {
|
|
throw new Error(`order_key backfill failed for ${stats.failed} table(s); will retry next run`)
|
|
}
|
|
},
|
|
}
|