Files
simstudioai--sim/apps/sim/lib/table/__tests__/lock-order.test.ts
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

108 lines
4.1 KiB
TypeScript

/**
* @vitest-environment node
*
* Lock-order regression guard: a column-creating CSV import must acquire the
* per-table row-order advisory lock (`user_table_rows_pos`) BEFORE writing
* `user_table_definitions`, matching the rows_pos → definitions order that plain
* inserts take via the row-count trigger. The opposite order deadlocks
* concurrent inserts on the same table.
*/
import { userTableDefinitions } from '@sim/db/schema'
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { importAppendRows } from '@/lib/table/import-data'
import type { TableDefinition } from '@/lib/table/types'
vi.mock('@sim/db', () => dbChainMock)
vi.mock('@/lib/core/config/feature-flags', () => ({
isFeatureEnabled: vi.fn().mockResolvedValue(false),
}))
vi.mock('@/lib/table/validation', () => ({
validateRowSize: vi.fn(() => ({ valid: true, errors: [] })),
validateRowAgainstSchema: vi.fn(() => ({ valid: true, errors: [] })),
coerceRowToSchema: vi.fn(() => ({ valid: true, errors: [] })),
coerceRowValues: vi.fn((row) => row),
validateTableName: vi.fn(() => ({ valid: true, errors: [] })),
validateTableSchema: vi.fn(() => ({ valid: true, errors: [] })),
getUniqueColumns: vi.fn(() => []),
checkUniqueConstraintsDb: vi.fn(async () => ({ valid: true, errors: [] })),
checkBatchUniqueConstraintsDb: vi.fn(async () => ({ valid: true, errors: [] })),
}))
const TABLE: TableDefinition = {
id: 'tbl-1',
name: 'People',
description: null,
schema: { columns: [{ name: 'name', type: 'string' }] },
metadata: null,
rowCount: 0,
maxRows: 1000,
workspaceId: 'ws-1',
createdBy: 'user-1',
archivedAt: null,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
}
/**
* invocationCallOrder of the first `tx.execute(...)` whose SQL contains
* `substring` — checks the template strings, the interpolated values (the
* advisory key is passed as a value), and `sql.raw` output.
*/
function executeOrderContaining(substring: string): number {
const { calls, invocationCallOrder } = dbChainMockFns.execute.mock
for (let i = 0; i < calls.length; i++) {
const arg = calls[i][0] as { strings?: unknown; values?: unknown; rawSql?: unknown } | undefined
const haystacks: string[] = []
if (Array.isArray(arg?.strings)) {
haystacks.push(
...(arg.strings as unknown[]).filter((s): s is string => typeof s === 'string')
)
}
if (Array.isArray(arg?.values)) {
haystacks.push(...(arg.values as unknown[]).filter((v): v is string => typeof v === 'string'))
}
if (typeof arg?.rawSql === 'string') haystacks.push(arg.rawSql)
if (haystacks.some((s) => s.includes(substring))) return invocationCallOrder[i]
}
return -1
}
/** invocationCallOrder of the first `tx.update(table)` call. */
function updateOrderForTable(table: unknown): number {
const { calls, invocationCallOrder } = dbChainMockFns.update.mock
for (let i = 0; i < calls.length; i++) {
if (calls[i][0] === table) return invocationCallOrder[i]
}
return -1
}
describe('table import lock ordering', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
})
it('acquires the rows_pos advisory before the user_table_definitions write when adding columns', async () => {
// The lock pre-acquire and the column-creating write both run at the top of
// the import, before the batch-insert loop. The loop's order-key aggregates
// aren't fully wired in this unit mock, so tolerate a downstream error after
// the locks under test have been recorded.
await importAppendRows(
TABLE,
[{ name: 'new_col', type: 'string' }],
[{ name: 'Alice', new_col: 'x' }],
{ workspaceId: 'ws-1', userId: 'user-1', requestId: 'req-1' }
).catch(() => {})
const rowsPosLockOrder = executeOrderContaining('user_table_rows_pos')
const definitionsWriteOrder = updateOrderForTable(userTableDefinitions)
expect(rowsPosLockOrder).toBeGreaterThan(0)
expect(definitionsWriteOrder).toBeGreaterThan(0)
expect(rowsPosLockOrder).toBeLessThan(definitionsWriteOrder)
})
})